Reference
Errors
The router's error codes, what each means, whether to retry, and how each surface reports them.
Error codes#
The WebSocket protocol, /v1/audio/speech and the LiveKit plugin share these codes. retryable says whether the same request can succeed if you try again.
bad_requestHTTP 400, not retryable- The request failed validation or a message arrived out of order. The message says which field or rule.
unauthorizedHTTP 401, not retryable- The API key is missing, wrong or deleted, or its org is disabled.
insufficient_creditsHTTP 402, not retryable- Your org is out of credit, so the router refuses new connections and requests until you buy more. Connections already open carry on.
provider_key_missingHTTP 400, not retryable- Your org has no key stored for the provider. Add one under Provider keys.
provider_rejectedHTTP 400, not retryable- The provider refused the request, e.g. a wrong stored provider key or an unknown voice. The provider’s message is passed through.
provider_rate_limitedHTTP 429, retryable- Your own quota at the provider is exhausted.
provider_unavailableHTTP 502, retryable- The provider returned a server error, or the connection to it failed or closed unexpectedly.
provider_timeoutHTTP 504, retryable- The provider took too long to send audio.
internalHTTP 500, retryable- The router failed.
The HTTP status is what /v1/audio/speech answers with. provider_status, when present, is the status the provider answered with.
How each surface reports them#
WebSocket (/v1/stream). An error message ends the segment; close the connection and open a new one. segment_id is null for an error before a valid start. Two refusals come before the connection opens, as the HTTP status of the handshake: 403 for an API key the router doesn’t accept, and 402 (insufficient_credits) when your org is out of credit.
{"type": "error", "segment_id": "c0ffee", "code": "provider_rejected", "message": "…", "retryable": false, "provider_status": 403}/v1/audio/speech. A JSON body with the status above. The status is sent with the first audio, so an error after audio has started can only abort the body: treat a response that ends early as failed.
HTTP/1.1 400 Bad Request{"error": {"code": "provider_key_missing", "message": "…", "retryable": false, "provider_status": null}}Provider drop-ins. In the provider’s own format and status, as the provider sent them, so your client’s error handling keeps working. The one error of the router’s own, insufficient_credits, comes as HTTP 402 in the provider’s error shape (on ElevenLabs’ WebSockets, as an in-band error and close 1008). See Sarvam, Raya and ElevenLabs.
In the LiveKit plugin#
Router errors become LiveKit's API errors, so LiveKit's retries and FallbackAdapter treat them as they treat any TTS's.
| What happened | Raised |
|---|---|
| A router error | APIStatusError, with the error’s retryable; status_code is provider_status or the status above |
| The router refused the API key | APIStatusError, status 403, not retryable |
| Your org is out of credit | APIStatusError, status 402, not retryable |
| The connection failed or dropped | APIConnectionError |
| The router went quiet after all text was sent | APITimeoutError |
| A bad setting at construction | ValueError |
REST API errors#
The provider-keys API, and the dashboard's own API, answer with a code and a message.
HTTP/1.1 401 Unauthorized{"error": {"code": "unauthenticated", "message": "invalid API key"}}| code | Status | Meaning |
|---|---|---|
| unauthenticated | 401 | The API key is missing, wrong or deleted. |
| bad_request | 422 | The request failed validation; the message names the field, never its value. |
| not_found | 404 | Nothing to delete: no key is stored for that provider. |
| internal | 500 | The router failed. |
The dashboard’s API adds these, from managing an org’s members. Each /api/orgs/… route also checks your role in the org: forbidden (403) when it isn’t enough, not_found (404) when you aren’t a member.
| code | Status | Meaning |
|---|---|---|
| already_member | 409 | The invited email already belongs to a member of the org. |
| already_invited | 409 | The invited email already has a pending invite to the org. |
| last_admin | 409 | The change would leave the org without an admin: make another member an admin first. |
| last_member | 409 | The org's last member can't leave it; orgs are never deleted. |