Errors and retries
Which failures are worth retrying, and which are not. Worth deciding before you write the retry loop.
HTTP status codes
| Code | Meaning | What to do |
|---|---|---|
200 | Success | — |
400 | Invalid parameters, or a rejected SDP/ICE payload | Fix the request. Do not retry unchanged |
401 | Missing or invalid API key | Check the header — see Authentication |
402 | Not enough credit to start | Surface a top-up prompt. Retrying will not help |
404 | Unknown or already-ended resource | For a realtime session, start a new one |
422 | Malformed body | Fix the request |
500 | Server error | Retry with backoff |
503 | Capacity or service unavailable | Retry with backoff — see below |
Two body shapes, both of which you have to handle
Most errors return a plain string in detail:
{ "detail": "Session not found: 550e8400-e29b-41d4-a716-446655440000" }
Conditions that carry a stable machine-readable code return an object instead:
{ "detail": { "code": "at_capacity", "message": "Realtime service is at capacity; please try again shortly" } }
So detail is a string or an object. Branch on the type:
const body = await response.json().catch(() => ({}));
const code = typeof body.detail === "object" ? body.detail?.code : undefined;
const message = typeof body.detail === "object" ? body.detail?.message : body.detail;
Branch on code, never on message — the wording is free to change.
At capacity is not an outage
Realtime analysis has finite concurrent capacity. When none is free,
POST /v2/realtime/session returns 503 with code: "at_capacity".
This is a designed state. Show it as "busy, try again in a moment", not as an error. Retry with backoff — and not in a tight loop, which will not get you in sooner.
Two things that surprise people:
GET /v2/realtime/healthkeeps reportinghealthythroughout. It reports whether the analysis service is reachable, not whether it has a free slot. Ahealthyresponse can be followed immediately by anat_capacitycreate.- Session creation can be slower than a typical REST call under load, because it reserves analysis capacity and mints TURN credentials. Set a generous client timeout.
Running out of credit mid-session
A realtime session can be ended by the server while streaming, if the balance is
exhausted. You receive an error frame on the results socket:
{
"type": "error",
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"error": "Out of credits — realtime session ended",
"code": "insufficient_credits"
}
Two consequences:
- Do not call
/endafterwards. The session is already torn down, so that call404s. The error frame is the end of the session. - This is the one path where a session ends without your asking. Handle it distinctly from a network drop, or your UI shows a hung stream.
Losing the connection
| What happened | How you see it | Recovery |
|---|---|---|
| WebSocket drops | onclose with no error frame | Not resumable. End the session, create a new one |
| WebRTC media stops | Peer connection state goes disconnected / failed | Not resumable. End the session, create a new one |
| No frames arriving | Socket open, nothing received | Check the peer connection actually reached connected. An open WebSocket says nothing about whether media is flowing |
There is no reconnect and no replay anywhere in the realtime API. Every recovery path is the same: end, create, renegotiate.
WebSocket close codes
The results socket refuses a connection with 1008 and one of these reasons:
Missing authentication token, Invalid or expired token,
Missing session_id query parameter, or Insufficient credits for realtime session.
Read the close reason — the four cases need different responses, and only the reason string distinguishes them.