Errors
Status codes, the error format, and which errors are safe to retry
The API uses standard HTTP status codes. A 2xx status means success. Anything else comes with a JSON body holding a readable message in error:
{
"error": "Missing 'text' in request body"
}Some errors add details. A rate-limit error carries the limit you reached, a word-limit error the words you sent and the maxWords allowed, and a text-length error the chars you sent and the maxChars allowed. Show the message to your developers, but branch your code on the status: messages can be reworded over time.
A few errors put a machine-readable code in error and the explanation in message: INSUFFICIENT_CREDIT, UNKNOWN_MODEL, UNKNOWN_VOICE, EMPTY_TEXT, TEXT_TOO_LONG and ENDPOINT_REMOVED. The per-IP limit's RATE_LIMITED is a code in error too, with no message. Branch on the code, not on the message.
A few others keep a message in Arabic in error and put the code in code: API_ACCESS_BLOCKED, MODEL_DISABLED, TOOL_DISABLED and MAINTENANCE. Branch on code for those.
Insufficient API credit
When your API credit can't cover a request, you get 402 with the code INSUFFICIENT_CREDIT, a message and the amounts in dinars:
{
"error": "INSUFFICIENT_CREDIT",
"message": "Not enough API credit. Top up your balance in the developer workspace.",
"required": 1.2,
"balance": 0.35,
"currency": "DZD"
}requiredis what the voice-over would have cost: its billable characters at the model's price.balanceis what you can spend right now: your API credit, less what your voice-overs still running have set aside.
While a voice-over runs, its price is set aside from your API credit, so the voice-overs you run at the same time can't spend more than you have between them. A voice-over is refused when what's left after the others can't cover it, even if your credit alone could. The price stays set aside until the voice-over is charged its cost, the moment it succeeds, or until it fails, which is charged nothing. Either way nothing stays set aside once a voice-over has ended.
Your plan's credits never pay for API requests, so this can happen on an Unlimited subscription too. Top up on the Developer Dashboard. To check before you send, ask Estimate Cost: its enough field tells you whether your credit covers a text. It doesn't subtract what your running voice-overs have set aside.
Unknown model or voice
A model that isn't one of the five models gets 400 with the code UNKNOWN_MODEL and the models you can use:
{
"error": "UNKNOWN_MODEL",
"message": "Unknown model 'voice213-ultra'. Use one of: voice213-lite, voice213-lite-2, voice213-pro, voice213-pro-1.5, voice213-pro-2 (see GET /v1/models).",
"models": ["voice213-lite", "voice213-lite-2", "voice213-pro", "voice213-pro-1.5", "voice213-pro-2"]
}A voice that isn't one of the 30 voice IDs gets 400 with the code UNKNOWN_VOICE and the full list of IDs in voices. In the inline format, an unknown voiceId gets 400 Invalid voice ID: '<id>'. Read the lists from GET /models and GET /tts/voices instead of hard-coding them.
Text that can't be spoken
A voice-over, or an estimate, needs words to speak. A text with nothing to speak, such as one made only of tone directions, gets 400 with the code EMPTY_TEXT:
{
"error": "EMPTY_TEXT",
"message": "Nothing to speak: the text is empty or contains only tone directions."
}A text over 5,000 words gets 400 Max 5000 words exceeded, with the words it has and the maxWords allowed. Every word counts, tone directions included, in both formats of POST /tts and in Estimate Cost. Split longer scripts into several requests.
Too much unspoken text
Tone directions of up to 100 characters, invisible characters and repeated spaces aren't billed, but they can't outweigh what the voice speaks. A text may be at most twice its billable characters plus 1,000 characters long, counting every character you send:
- Billable characters are the ones Voice213 bills, before the 100-character minimum: the
charsa voice-over or an estimate returns. Below 100, they count as 100. - Every character counts toward the length, tone directions, spaces and invisible characters included. Characters are Unicode code points: an Arabic letter, or an emoji, is one character (in JavaScript,
[...text].length, nottext.length).
So a text with 150 billable characters can be up to 1,300 characters long, and one with 2,000 up to 5,000. An ordinary script with tone directions is far from this limit. A longer text gets 400 with the code TEXT_TOO_LONG, the characters you sent in chars and the most you could send in maxChars:
{
"error": "TEXT_TOO_LONG",
"message": "Too much of the text isn't spoken: tone directions, punctuation and invisible characters can't be more than about the spoken text itself. Remove them or split the text.",
"chars": 1842,
"maxChars": 1300
}This applies in both formats of POST /tts and in Estimate Cost, and nothing is charged. Tone directions stay free: remove the ones you don't need, or the padding, and send the text again.
API access switched off
Voice213 can switch off API access for an account. Every request made with its key then gets 403, with the code API_ACCESS_BLOCKED in code and a message in Arabic in error:
{
"error": "تم إيقاف وصولك إلى واجهة المطورين. تواصل مع الدعم.",
"code": "API_ACCESS_BLOCKED"
}While it's off, you also can't create a key or switch yours back on. Don't retry: contact support.
Paused models and maintenance
Voice213 can pause a voice model for the API, pause voice-overs through the API, or take the API down for maintenance. Requests then get 503, with a code in code, a message in Arabic in error, and the same message in Arabic, English and French in messages:
| Code | When | Which requests |
|---|---|---|
MODEL_DISABLED | The model you asked for is paused for the API. model names it. | POST /tts with that model. GET /models lists it with "available": false. |
TOOL_DISABLED | Voice-overs through the API are paused. tool is public_api. | POST /tts. Estimates and the other endpoints keep working. |
MAINTENANCE | The API is down for maintenance. endsAt is when it's expected back, or null. | Every endpoint under /api/v1. /health keeps answering. |
{
"error": "هذا النموذج الصوتي غير متاح مؤقتًا. اختر نموذجًا آخر أو حاول لاحقًا.",
"code": "MODEL_DISABLED",
"model": "voice213-pro-2",
"messages": {
"ar": "هذا النموذج الصوتي غير متاح مؤقتًا. اختر نموذجًا آخر أو حاول لاحقًا.",
"en": "This voice model is temporarily unavailable. Pick another model or try again later.",
"fr": "Ce modèle de voix est temporairement indisponible. Choisissez-en un autre ou réessayez plus tard."
}
}{
"error": "هذه الأداة متوقفة مؤقتًا. يرجى المحاولة لاحقًا.",
"code": "TOOL_DISABLED",
"tool": "public_api",
"messages": {
"ar": "هذه الأداة متوقفة مؤقتًا. يرجى المحاولة لاحقًا.",
"en": "This tool is temporarily paused. Please try again later.",
"fr": "Cet outil est temporairement suspendu. Veuillez réessayer plus tard."
}
}{
"error": "المنصة قيد الصيانة حاليًا. سنعود قريبًا، شكرًا على صبرك.",
"code": "MAINTENANCE",
"messages": {
"ar": "المنصة قيد الصيانة حاليًا. سنعود قريبًا، شكرًا على صبرك.",
"en": "Voice213 is down for maintenance. We'll be back soon, thanks for your patience.",
"fr": "Voice213 est en maintenance. Nous revenons très vite, merci de votre patience."
},
"endsAt": "2026-10-01T06:00:00.000Z"
}The texts in error and messages can differ from these, so branch on code. While endsAt is set and still ahead, a MAINTENANCE answer also has a Retry-After header with the seconds left until then. Maintenance doesn't end on its own at endsAt: past that time the answer keeps the old endsAt and has no Retry-After, so retry after a short wait. Nothing is charged, and these requests don't count toward your limits.
MODEL_DISABLED: send the voice-over with another model. OnceGET /modelsno longer shows"available": falsefor it, the model is back.TOOL_DISABLEDandMAINTENANCE: retry later.
Removed endpoints
The API is for voice only. These endpoints were removed, and answer 410 Gone whatever you send, with or without a valid key. While the API is down for maintenance they answer 503 MAINTENANCE, like every endpoint:
| Endpoint | Where to do it now |
|---|---|
POST /generate-script | Script Writer in the web app |
POST /generate-angles | Script Writer in the web app |
POST /generate-tone | Write the directions yourself from GET /tts/tones, or use Auto Generate with AI on the web app's Text to Speech page |
POST /tashkiil | Tashkeel in the web app |
POST /product-image | AI Photo in the web app |
POST /landing-page | Landing Pages in the web app |
{
"error": "ENDPOINT_REMOVED",
"message": "POST /v1/tashkiil is no longer part of the Voice213 API. The API now covers voice only: see https://docs.voice213.com/api-reference/introduction"
}Nothing is charged for these requests. Don't retry them: remove the calls from your code.
Status codes
| Status | Meaning | Retry? |
|---|---|---|
200 | Success. | |
400 | The request is invalid: a required field is missing, the model or voice is unknown, the text has nothing to speak, is over the word limit or is mostly unspoken, or the body isn't valid JSON. | No. Fix the request. |
401 | The API key is missing, wrong, switched off or deleted. | No. Check the key. |
402 | Your API credit, less what your running voice-overs have set aside, can't cover the request. | After you top up, or once your running voice-overs end. |
403 | The account that owns the key is deactivated or suspended, or its API access is switched off. | No. |
404 | There's no such endpoint. | No. Check the method and path. |
410 | The endpoint was removed from the API. | No. Remove the call. |
413 | The request body is over 25 MB. | No. Send less. |
429 | Too many requests: your key's per-minute or per-day limit, 20 voice-overs already running with your key, or too many requests from your IP address. | Yes, after a pause. |
500 | Something went wrong on our side, including a failed voice-over in the inline format. | Yes, with backoff. |
502 | The engine couldn't complete the voice-over. | Yes, with backoff. |
503 | The model you asked for or voice-overs through the API are paused, or the API is down for maintenance. | Later. For MODEL_DISABLED, switch to another model. |
Voice213 charges a request only once its voice-over has succeeded, so a request that fails with an error costs nothing. A failed voice-over only says that it failed: the engine's own error isn't passed on.
Common errors
| Status | Error | What to do |
|---|---|---|
400 | Missing 'text' in request body | Send text as a non-empty string (voice-over in the inline format). |
400 | text is required | Send text as a non-empty string (voice-over in the studio format, estimates). If you did, check Content-Type: application/json: without it the body is ignored. |
400 | UNKNOWN_MODEL | Send one of the models from GET /models, such as voice213-lite. |
400 | UNKNOWN_VOICE | Send one of the 30 voice IDs, such as nariman. |
400 | Invalid voice ID: 'sarah' | In the inline format, voiceId must be one of the 30 voice IDs. |
400 | EMPTY_TEXT | The text has nothing to speak, such as only tone directions. Add the words to say. |
400 | Max 5000 words exceeded | A voice-over or an estimate takes up to 5,000 words, tone directions included, in either format. The body has words and maxWords. Split the script. |
400 | TEXT_TOO_LONG | Too much of the text isn't spoken: tone directions, spaces or invisible characters. The body has chars and maxChars. Remove what you don't need. See Too much unspoken text. |
400 | Invalid JSON body | Check that the body is valid JSON. |
401 | Missing API key. Pass it as 'Authorization: Bearer sk_live_...' | Send your key. |
401 | Invalid or inactive API key | Check the key on the API Keys page. |
402 | INSUFFICIENT_CREDIT | Top up your API credit, or wait for your running voice-overs to end. |
403 | Account deactivated | Contact support. |
403 | API_ACCESS_BLOCKED (in code) | API access is switched off for your account. Contact support. See API access switched off. |
404 | Not found | Check the method and path, such as POST /api/v1/tts. |
410 | ENDPOINT_REMOVED | The endpoint is no longer part of the API. See Removed endpoints. |
413 | Request body too large | Keep the body under 25 MB. |
429 | Rate limit exceeded: RPM limit exceeded | Wait for the minute to pass. |
429 | Rate limit exceeded: Daily limit exceeded | Wait until the next day. |
429 | Rate limit exceeded: Concurrent request limit exceeded | Your key already has 20 voice-overs running. Retry when one of them ends. |
429 | RATE_LIMITED | Too many requests from your IP address. Wait Retry-After seconds. |
500 | Internal server error processing TTS request | Retry. |
500 | Server error | Retry. Nothing was charged. |
500 | Internal server error during authentication | Retry. |
502 | Failed to generate audio. No points consumed. | Retry. Nothing was charged. |
503 | MODEL_DISABLED (in code) | The model is paused for the API. Use another model. See Paused models and maintenance. |
503 | TOOL_DISABLED (in code) | Voice-overs through the API are paused. Retry later. |
503 | MAINTENANCE (in code) | The API is down for maintenance. Retry after Retry-After seconds when it's there, or later. |
A few messages still say "points" from an earlier version of the API. They mean nothing was charged to your API credit.
Retrying safely
Retry 429, 500, 502 and 503 after a pause, and back off exponentially between attempts. Honor Retry-After when it's there. A 429 for your key's daily limit won't clear until the next day, so stop retrying when the message says Daily limit exceeded. A 503 with the code MODEL_DISABLED can last a while too: send the voice-over with another model instead. See Going to Production for a retry helper.
Was this page helpful?