Handle errors, retries and rate limits
Every error has a code and a fix. Which to retry, how long to wait, and a retry helper that respects retry_after_seconds.
Every error reply has error, message, fix and docs fields.
| Status | Code | What to do |
|---|---|---|
| 401 | unauthorized | Key missing, wrong or revoked. Do not retry. |
| 400 | invalid_json / text_required_max_500_chars | Fix the request. Text is 1 to 500 characters. |
| 402 | free_allowance_used | Free allowance spent. Do not retry. |
| 403 | key_scope | A scoped key cannot call this route. Use an account key on your server. |
| 404 | not_in_library / voice_not_available_for_text | That voice only plays library sentences. Pick a live voice or a library line. |
| 404 / 422 | custom_voice_error | The voice id is unknown or not ready. Check the reason field. |
| 409 | storage_quota | Private audio storage is full; delete unused voices or wait for old audio to expire. |
| 422 | language_not_supported / generation_failed_quality | Fix the language, or add a full stop or reword the text. |
| 429 | rate_limited | Wait retry_after_seconds, then retry. |
| 429 | live_rate_limited | Too many brand-new sentences per minute (20 free, 120 paid). Prepare them ahead with /v1/prepare. |
| 502/503/504 | upstream_unavailable and gateway errors | Usually the first brand-new sentence after a quiet period. Retry once. |
Limits: 15 requests per second per account on the free plan (100 paid), counted over 10 seconds; up to 5 active keys; up to 3 webhooks.
A retry helper
JavaScript (server)
async function speak(text, voice = "af_heart", tries = 3) {
for (let i = 0; i < tries; i++) {
const r = await fetch("https://speakvora.com/api/v1/speak", {
method: "POST",
headers: { "x-api-key": process.env.SPEAKVORA_KEY, "content-type": "application/json" },
body: JSON.stringify({ text, voice, lang: "en" }),
});
if (r.ok) return (await r.json()).url;
const j = await r.json().catch(() => ({}));
if (r.status === 429) { await new Promise(s => setTimeout(s, (j.retry_after_seconds || 1) * 1000)); continue; }
if ([502, 503, 504].includes(r.status)) { await new Promise(s => setTimeout(s, 500 * (i + 1))); continue; }
throw new Error(j.error || "http_" + r.status); // other 4xx: do not retry
}
throw new Error("gave_up");
}Related: reusable phrases with live speech and how billing counts characters.