Skip to content

Errors and retries

Errors come in two places.

  • Before a connection exists, as an HTTP answer with a JSON body: {"error":{"code":"…","message":"…","request_id":"req_…"}}. This covers POST /v1/sessions and the WebSocket upgrade.
  • During a session, as an error event with code, message, retryable and fatal. An error event has no request ID. If fatal is true, the session ends next with session.ended. If not, the session carries on.

Match on code, never on the message text. New codes can be added: treat one you don’t know as a failure and look at the HTTP status. Keep the request_id of every HTTP answer: it is what lets us find the request. For an error event, keep the session ID and the code.

Code HTTP status Fatal in a session What to do
invalid_request 400 no Fix the request or the event. details.field names the field. A replayed Idempotency-Key that doesn’t match, or whose session is already live, answers 409 with this code: send a new Idempotency-Key for a new request, and reuse the same key only to retry the same request.
invalid_audio none (event only) no Fix the audio: PCM16, mono, frames of 10 to 500 ms, no faster than 2 times real time. The bad frame is dropped.
unauthenticated 401 yes The API key or client secret is missing or wrong. Check the key and the Authorization header.
secret_expired 401 yes The client secret is older than about 60 s or was already used. Create a new session.
forbidden 403 yes The key lacks the scope, the account is suspended, or the monthly minutes are used up (details.reason). Retrying won’t help.
origin_not_allowed 403 yes Browsers only: add the page’s origin in your console (see Security). A server client must not send an Origin header at all.
voice_not_found 404 no The voice is unknown, retired or not enabled for you. List yours with GET /v1/voices.
session_not_found 404 yes No such session for your account. Check the ID.
capacity_exhausted 503 with Retry-After no Kheelona is full right now. Wait the Retry-After seconds (1 to 30), then try again. At connect, Retry-After: 1 means only the server you reached is busy: connect again with the same client secret after a second. When details.reason is voice_service_starting (waking up, Retry-After: 60) or voice_service_asleep (switched off, Retry-After: 300), the voice service is not running: wait that long.
partner_limit_reached 429 no Your account is at its concurrent-session limit. Wait for a session to end. Ask the Kheelona team if you need more.
rate_limited 429 with Retry-After no Too many API calls, or typed turns sent too fast in a session. Wait the Retry-After seconds (during a session, about a second) and slow down.
provider_unavailable none (event only) no, then yes A step of the reply failed. Usually the voice asks the person to say it again. If the voice’s own speech failed, there is no spoken line: the reply just ends with reply.done and reason: error. Either way the session goes on. After 3 failures in a row the error is fatal and the session ends as provider_failure.
internal 500 yes Our fault. Quote the request_id. Try again once after a short wait.
not_found 404 not used REST only: the resource doesn’t exist, or isn’t visible to your key.
conflict 409 not used REST only: the request clashes with the current state. Change the request.
unavailable 503 with Retry-After not used REST only: the API is briefly unavailable. Wait the Retry-After seconds and try again.

“Not used” means the code is never sent on the WebSocket. not_found, conflict and unavailable appear only on REST calls.

Retry only what can succeed.

  • Honour Retry-After. When an answer has it, wait at least that many seconds. It is in whole seconds. Don’t retry sooner, and don’t retry in parallel. The example clients wait up to 30 seconds and give up after 3 attempts.
  • Retry on 429 and 503 (rate_limited, capacity_exhausted, partner_limit_reached, unavailable). Use one Idempotency-Key for all attempts, so a retry can never make a second session. For a 500 internal, one retry after a short wait is fine.
  • Don’t retry 400, 401, 403 or 404. They fail the same way each time. Fix the cause.
  • secret_expired means a new session. Create one with a fresh Idempotency-Key and connect at once. Connect within the 60 seconds: don’t hold a secret in a queue.
  • A failed connection means a new session. Once the voice was ready, the client secret is used up, and a failure before that may have spent it too. Creating a new session is always safe.
  • capacity_exhausted is normal at busy moments. Show the user a short, friendly “busy” message and retry later rather than looping. See Limits. If details.reason is voice_service_starting or voice_service_asleep, the voice service is waking up or switched off: tell the user it will be back shortly and retry after the Retry-After seconds (60 or 300).
  • partner_limit_reached usually means sessions are left open. Make sure every session ends, with session.end or POST /v1/sessions/{id}/end.
  • rate_limited: slow down. On POST /v1/sessions it means too many requests. During a session it means typed turns (input.text) came faster than 5 at once, then 1 per second: that turn was dropped, the error is not fatal and the session goes on, so send the turn again after a second. At connect it means the client secret was refused, and many refusals came from your address in the last minute: don’t retry that secret. Find why connects are refused (an expired secret, a wrong origin), then create a new session, after Retry-After. A valid secret still connects from the same address.

Give up after a few attempts and tell the user. A retry loop with no end can make a busy moment worse for everyone.

A browser can’t connect and the SDK reports network. The WebSocket upgrade was refused or never completed, and a browser doesn’t show why. Check two things before anything else:

  • The origin list. The page’s exact origin (scheme, host and port) must be in your console’s allowed origins. The server answers origin_not_allowed, but the browser only tells your code the connection failed. See Security.
  • The client secret’s age. A secret is good for about 60 seconds and works once. If the session was created earlier, or a first attempt already used the secret, create a new session and connect at once.

If both are right, quote the session ID to us.

Every HTTP error answer has a request_id. An error event during a session has none: quote the session ID and the error code instead. The example relays print what they have on standard error as error <code> session <session id> request_id <id>, with session only once the session existed and - where there is no request ID. Send us that line and the time, and we can find the session. It holds no conversation.