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 coversPOST /v1/sessionsand the WebSocket upgrade. - During a session, as an
errorevent withcode,message,retryableandfatal. Anerrorevent has no request ID. Iffatalis true, the session ends next withsession.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.
Every code
Section titled “Every 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.
The retry policy
Section titled “The retry policy”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 oneIdempotency-Keyfor all attempts, so a retry can never make a second session. For a 500internal, 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_expiredmeans a new session. Create one with a freshIdempotency-Keyand 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_exhaustedis normal at busy moments. Show the user a short, friendly “busy” message and retry later rather than looping. See Limits. Ifdetails.reasonisvoice_service_startingorvoice_service_asleep, the voice service is waking up or switched off: tell the user it will be back shortly and retry after theRetry-Afterseconds (60 or 300).partner_limit_reachedusually means sessions are left open. Make sure every session ends, withsession.endorPOST /v1/sessions/{id}/end.rate_limited: slow down. OnPOST /v1/sessionsit 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, afterRetry-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.
Troubleshooting
Section titled “Troubleshooting”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.
Quote the request ID
Section titled “Quote the request ID”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.