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.

StatusCodeWhat to do
401unauthorizedKey missing, wrong or revoked. Do not retry.
400invalid_json / text_required_max_500_charsFix the request. Text is 1 to 500 characters.
402free_allowance_usedFree allowance spent. Do not retry.
403key_scopeA scoped key cannot call this route. Use an account key on your server.
404not_in_library / voice_not_available_for_textThat voice only plays library sentences. Pick a live voice or a library line.
404 / 422custom_voice_errorThe voice id is unknown or not ready. Check the reason field.
409storage_quotaPrivate audio storage is full; delete unused voices or wait for old audio to expire.
422language_not_supported / generation_failed_qualityFix the language, or add a full stop or reword the text.
429rate_limitedWait retry_after_seconds, then retry.
429live_rate_limitedToo many brand-new sentences per minute (20 free, 120 paid). Prepare them ahead with /v1/prepare.
502/503/504upstream_unavailable and gateway errorsUsually 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.