Classify any failure in one lookup, and know before you write the handler which ones are worth retrying. Every table on this page is read off the code that raises the error.

Two envelopes

The speech surfaces answer in the OpenAI error shape, because an OpenAI SDK parses that without a translation layer:
That covers /v1/audio, /v1/chat, /v1/models, /v1/realtime and /v1/voices — including their authentication failures, and including request validation, which comes back as a 400 rather than FastAPI’s 422. Everything else — interact, story, sessions, memory, keys, organizations — answers FastAPI’s shape:
Branch on code where there is one; fall back to the status. Both SDKs read both envelopes and put the server’s own sentence on ApiError.message — see SDKs.

HTTP status and code

The three 429s need three different responses, and telling them apart is the whole reason they carry distinct codes: rate_limit_exceeded means slow down, free_tier_daily_limit means come back tomorrow or pay, insufficient_quota means pay. Only the first is worth a retry of the same request.

Three ways a model id fails

They are different mistakes and they answer differently, so do not collapse them into one handler. Each message lists the ids that would have worked for that endpoint in this deployment, which is usually the whole answer. 503 model_unavailable is a deployment fact rather than a client mistake — nothing you can change in the request fixes it.
/v1/realtime resolves models through the registry directly rather than through that explaining layer, so all three cases arrive there as the single 404 model_not_found — and on the socket, as a 1008 close carrying that message.

WebSocket close codes

A live socket has no status line, so refusals arrive as close codes. The reason carries the first 120 characters of the message. 1008 is the one that carries meaning. It is used for every connect-time refusal: an unresolvable model, a failed credential, an exhausted balance, a spent free day, and the per-minute rate limit.

A limit reached mid-call

The balance and the free-tier day are re-checked once a minute while a session is up, so a call can meet a limit it did not have at connect. Before the 1008, the gateway sends one more eesi.session frame that says why:
Show limit.message. A client that ignores this frame tells the person “connection lost”, which is both wrong and unactionable. Note that resume.supported is false on it. Drop the handle rather than redialling with it — the connect-time gate would refuse the resume for the same reason, and a redial loop against a spent balance is the failure mode this frame exists to prevent. Only insufficient_quota and free_tier_daily_limit end an established session this way; a re-check that could not decide — because the ledger or the ceiling store was unreachable — refuses a new session but never cuts one that is already up.

What is worth retrying

Retry with backoff: 429 rate_limit_exceeded, 502, 503, and a 1006 close. These are conditions that clear on their own. Do not retry: anything 4xx that names a fix — a bad voice id, an unknown model, a validation failure, an exhausted balance. Retrying a 429 insufficient_quota in a loop is how an outage becomes a rate-limit ban on top of an outage. Never retried for you: the SDKs retry a GET once on a connection failure and nothing else, because a POST that failed on the wire may still have reached the server. A retried voice clone is a second 100-credit charge; a retried synthesis is a second bill. Retry a write only when you know it is safe to repeat.

Reading an error in code

Next

Pricing and limits

The ceilings behind every 429.

Connections that survive

Close codes, keepalives and resumption in full.

Authentication

What a 401 or a 403 on a key actually means.

SDKs

ApiError, its fields, and the retry policy.