Error Handling and Retries

Retry strategy for rate limits and server errors

Overview

The Speechify API returns standard HTTP status codes. Transient errors (rate limits, server errors) can be retried safely. Client errors (bad input, auth failures) require fixing the request.

Error types

Transient errors (retry safe)

These errors are temporary. Retry with exponential backoff.

StatusMeaningAction
429 Too Many RequestsRate or concurrency limit exceededWait for Retry-After header, then retry
502 Bad GatewayUpstream failureRetry with backoff
503 Service UnavailableTemporary overload or maintenanceWait for Retry-After header, then retry
504 Gateway TimeoutUpstream timeoutRetry with backoff

Persistent errors (fix the request)

These errors indicate a problem with the request itself. Retrying without changes will fail again.

StatusMeaningAction
400 Bad RequestInvalid input, malformed JSON, or unsupported parameterCheck request body and parameters
401 UnauthorizedMissing or invalid API keyVerify Authorization header
402 Payment RequiredInsufficient balance or spend limit reachedTop up balance or raise spend cap
400 content_policy_violationText refused by the content policyEdit the input; retrying unchanged will fail again
403 ForbiddenAuthenticated but not authorizedConfirm workspace and permissions
404 Not FoundResource does not existVerify resource ID
409 ConflictIdempotency key reused with different bodyUse a fresh key or match the original body

Voice-cloning consent verification has its own error family (consent_*) spanning 404, 409, 422 and 502, and three codes share 422, so branch on the code rather than the status. Each code and its fix is listed in When verification fails.

Agent runs

Several run codes share a status, so branch on code here too.

CodeStatusWhat it meansWhat to do
durable_runs_not_in_plan402The workspace is not granted durable_runs_accessTalk to us; no plan grants it yet
agent_publish_gate_required422The agent’s current configuration has not passed its publish gatePOST /v1/agents/{agent_id}/publish, then retry. Required again after every configuration change
tool_transport_unsupported422An attached MCP tool uses the legacy sse transport, which runs cannot executeSwitch the tool to http_streamable or detach it. The message names it
concurrency_limit_reached429The workspace already has 200 runs queued or runningNo Retry-After - what frees a slot is one of your own runs ending. Follow them and start the next then
spend_budget_exceeded402The workspace is over its monthly spend budgetRaise the budget or wait for the reset
agent_run_not_pending409You submitted an approval for a run that is not waiting on oneRe-read the run; it was decided or it moved on
agent_run_action_stale409The approval you decided is no longer the run’s pending actionRe-read pending_action and decide the current one

Stores

CodeStatusWhat it means
store_limit_reached409The workspace is at its plan’s store count
store_document_limit_reached409The store is at its plan’s document count
store_bytes_limit_reached409The store is at its plan’s byte ceiling - the one a document library reaches first
store_document_version_conflict412The document was written since the revision your If-Match named; re-read it and reapply

Hosted APIs

Answered on your own subdomain to your API’s callers, so they read like any other envelope. The daily caps release a slot the moment a request they admitted fails.

CodeStatusWhat it meansWhat to do
hosted_api_not_found404No live API answers this hostThe slug is wrong, the API is deleted, or its workspace is not granted hosted APIs
api_route_not_found404No route matches the pathRead the API’s /openapi.json
route_read_limit_reached429The API has served its daily_read_cap from storage todayRaise the cap, or cache the route so repeats are not reads
route_run_limit_reached429The API has started its daily_run_cap runs todayRaise the cap; a retried Idempotency-Key still replays its run
route_write_limit_reached429The API has landed its daily_write_cap documents todayRaise the cap; a replayed Idempotency-Key is not a write
route_run_failed502The run behind a run route could not start or ended without a usable resultThe body carries the run handle; the message names the cause
route_output_unavailable503A run_latest route has no succeeded run to serve yet, or its data could not be readWait for the schedule, or check the trigger
hosted_api_public_refused403The workspace’s policy does not allow an API open to the internetAn owner or admin changes hosted_apis_public_allowed under workspace settings; this one is answered to the API’s owner on /v1/apis, not to its callers

Server errors (retry cautiously)

StatusMeaningAction
500 Internal Server ErrorUnexpected server failureRetry cautiously with cap (3-5 attempts max); often not transient

A 500 error often signals a request that will keep failing (e.g., a bug triggered by specific input). Retry a few times, but stop if it persists. Log the Speechify-Request-Id and contact support.

Retry strategy

Use exponential backoff with jitter for transient errors:

import time
import random
from speechify import Speechify
client = Speechify()
def generate_with_retry(text, max_retries=5):
for attempt in range(max_retries):
try:
return client.audio.speech(
input=text,
voice_id="geffen_32",
model="simba-3.2",
audio_format="mp3",
)
except Exception as e:
status = getattr(e, 'status_code', None)
# Transient errors: retry with backoff
if status in [429, 502, 503, 504] and attempt < max_retries - 1:
# Check for Retry-After header
retry_after = getattr(e, 'headers', {}).get('Retry-After')
if retry_after:
time.sleep(int(retry_after))
else:
# Exponential backoff with jitter
delay = (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
# 500: retry cautiously
elif status == 500 and attempt < 3:
delay = (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
else:
raise

Retry-After header

When present, Retry-After tells you how long to wait (in seconds) before retrying. Respect this value to avoid hammering the API.

HTTP/1.1 429 Too Many Requests
Retry-After: 5

Wait at least 5 seconds before the next attempt.

Idempotency on retries

Retrying a mutating POST (anything that creates a call, batch, purchase, or resource) risks performing the action twice if the first attempt succeeded but the response was lost.

Only endpoints that explicitly document Idempotency-Key support will replay the original response. Check the endpoint reference before relying on idempotency protection.

For supported endpoints, send an Idempotency-Key header with a unique value per logical operation. Reuse the same key on every retry:

curl -X POST https://api.speechify.ai/v1/agents/outbound-calls \
-H "Authorization: Bearer $SPEECHIFY_API_KEY" \
-H "Idempotency-Key: 4c7f0a78-0a61-42b4-8b8a-60b307b18f0c" \
-H "Content-Type: application/json" \
-d '{"agent_id": "agent_demo0001", "to": "+15555550123"}'

The server runs the request once and replays the original response on retries. Endpoints that do not document idempotency support will ignore the header. See Idempotency for details.

Logging and debugging

Always log the Speechify-Request-Id from error responses. Include it when contacting support:

{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 10 seconds."
},
"request_id": "req_7f3a2c1b4d5e6f7a"
}

The request_id uniquely identifies the failed request in Speechify’s logs.

FAQ

No. A 4xx (except 429) means the request itself is wrong. Retrying it unchanged will fail again. Fix the input, auth, or resource ID, then try a new request.

For transient errors (429, 502, 503, 504), retry 3-5 times with exponential backoff. For 500, retry cautiously (2-3 times max) and stop if it persists.

HTTP chunked responses (e.g., /v1/audio/stream) cannot send error messages after the stream starts. If the connection closes early, check total bytes received and retry the remaining text. See Streaming.