# Errors and retries

> Every error code, whether it ends the session, and what to do. How to retry without making things worse.

Source: https://docs.voice.kheelona.com/guides/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`.

## 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](https://docs.voice.kheelona.com/guides/security.md#allowed-origins)). 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

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](https://docs.voice.kheelona.com/concepts/limits.md). 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.

## 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](https://docs.voice.kheelona.com/guides/security.md#allowed-origins).
- **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

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.
