Errors come back with an HTTP status and a JSON body:
{
"error": {
"code": "unknown_voice",
"message": "Voice `kate` isn't available for model `v1`. See GET /v1/voices."
}
}Branch on code, which stays the same; message is written for people and may change.
Error codes
| Status | Code | Meaning | What to do |
|---|---|---|---|
| 400 | invalid_request | The body isn't valid JSON or has the wrong shape. | Fix the request; the message says what's wrong. |
| 400 | empty_text | text is empty, or only whitespace. | Send some text. |
| 400 | text_too_long | text is over 5,000 characters. | Split it into several requests, ideally at sentence boundaries. |
| 400 | unknown_voice | No such voice for that model, or a cloned voice from another account. | Check the id with GET /v1/voices. |
| 400 | invalid_temperature | temperature is outside 0 to 2. | Use a value in range, or leave it out. |
| 400 | invalid_language | language isn't one of en, es, fr, de, ja, zh. | Use one of those, or leave it out. |
| 401 | unauthorized | The API key is missing, wrong or revoked. | Send Authorization: Bearer io_live_… with a current key. |
| 402 | insufficient_credits | Your credit balance is used up. | Add credit or turn on auto-recharge in Billing. |
| 404 | not_found | No such endpoint, or a cloned voice that doesn't exist. | Check the path and id. |
| 502 | upstream_error, upstream_unreachable | Generation failed on our side. You aren't charged. | Retry with backoff. |
| 503 | busy | Every slot for that model was taken for 3 seconds. | Retry with backoff. |
| 500 | internal | Something broke on our side. | Retry; if it keeps happening, tell us the X-Request-Id. |
Speech to text adds invalid_response_format and, like cloning, missing_audio, invalid_audio and audio_too_long.
Cloning adds its own codes, all 400: consent_required, invalid_name, invalid_transcript, missing_audio, invalid_audio, audio_too_short, audio_too_long and too_many_voices. Each message says exactly what to fix.
Retrying
Retry 502, 503 and 500 with exponential backoff and jitter, for example after 0.5 s, 1 s, 2 s and 4 s. Don't retry 4xx errors unchanged: they fail the same way every time.
import random, time, requests
def tts(payload, key, attempts=5):
for i in range(attempts):
r = requests.post("https://api.iodine.dev/v1/tts", json=payload,
headers={"Authorization": f"Bearer {key}"})
if r.status_code not in (500, 502, 503):
r.raise_for_status()
return r.content
time.sleep(0.5 * 2 ** i + random.random() * 0.25)
r.raise_for_status()Limits
| Text per request | 5,000 characters |
| Waiting for a free slot | up to 3 seconds, then 503 busy |
| Realtime idle timeout | 5 minutes without a message |
| Realtime message size | 256 KB |
| Cloned voice recording | 3 to 30 seconds, up to 20 MB |
| Cloned voices per account | 50 |
| Speech to text upload | 25 MB, 10 minutes |
Need more throughput or higher limits? Email business@iodine.dev.