> ## Documentation Index
> Fetch the complete documentation index at: https://docs.chattler.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> One envelope, stable codes, and a request id on every response.

## The envelope

Every failure, whatever the status, is one JSON object:

```json theme={null}
{
  "error": {
    "code": "insufficient_balance",
    "message": "Insufficient balance",
    "request_id": "7f20a5f0-5dde-48cf-94aa-38acbbd218e0"
  }
}
```

* `code` is stable and machine-readable. Branch on it, not on `message`.
* `message` is a human-readable explanation and may change.
* `request_id` is the UUID of the request. It is also returned as the `X-Request-ID` header on **every** response, success or failure. Quote it when reporting a problem.

On the streaming endpoint a failure that happens after the stream has started arrives as an `error` event with the same three fields; see [Streaming](/guides/streaming).

## Codes

| HTTP | Code                         | Meaning                                                                                                               |
| ---- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| 401  | `invalid_api_key`            | Missing, malformed, unknown, deleted or wrong-secret key                                                              |
| 403  | `agent_mismatch`             | The key belongs to another agent                                                                                      |
| 403  | `permission_denied`          | A `read-only` key called a chat endpoint                                                                              |
| 403  | `agent_disabled`             | The agent is switched off. Chat endpoints only                                                                        |
| 404  | `agent_not_found`            | The agent no longer exists. Chat endpoints only                                                                       |
| 404  | `conversation_not_found`     | Unknown conversation, or one that is not this key's                                                                   |
| 402  | `insufficient_balance`       | The owner's balance cannot cover the reply; nothing was charged                                                       |
| 409  | `idempotency_conflict`       | The same `Idempotency-Key` was used with a different body                                                             |
| 409  | `idempotency_in_progress`    | The original request is still running; retry after `Retry-After` seconds (always `2`)                                 |
| 409  | `conversation_busy`          | Another message for the same external user is being answered; retry after `Retry-After` (always `2`)                  |
| 422  | `validation_error`           | Bad body, header, date, limit or cursor                                                                               |
| 429  | `rate_limit_exceeded`        | The key's window is exhausted; see [Rate limits](/guides/rate-limits)                                                 |
| 503  | `api_rate_limit_unavailable` | The rate limiter is down; the request is refused rather than served unmetered. Retry after `Retry-After` (always `5`) |
| 503  | `service_unavailable`        | A backend the API depends on is down; retry after `Retry-After` (always `2`)                                          |
| 500  | `internal_error`             | Unexpected failure; quote the `request_id`                                                                            |

`agent_not_found` and `agent_disabled` are checked only by `POST …/chat` and `POST …/chat/stream`. The two `GET` endpoints do not look at the agent's state: a key of a disabled agent can still list its conversations and read messages.

## What to retry

<AccordionGroup>
  <Accordion title="Retry after Retry-After, same Idempotency-Key">
    `idempotency_in_progress`, `conversation_busy`, `rate_limit_exceeded`, `api_rate_limit_unavailable`, `service_unavailable`. Reusing the key guarantees you never pay twice for the same message.
  </Accordion>

  <Accordion title="Fix the request first">
    `validation_error` (read `message`; it names the offending field), `idempotency_conflict` (use a new key for the new body), `permission_denied` (use a `full` key), `agent_mismatch` (use the agent the key belongs to).
  </Accordion>

  <Accordion title="Do not retry automatically">
    `invalid_api_key` (the key was deleted or the secret is wrong), `agent_disabled`, `agent_not_found`, `insufficient_balance` (top up the balance first). A retry loop on these only burns the rate limit.
  </Accordion>

  <Accordion title="Report with the request id">
    `internal_error`. Retry once with the same `Idempotency-Key`: the failed record is released, so the retry runs the request again rather than replaying a stored result. That is expected, but not guaranteed to be free. If the failure happened after the model had already answered, the retry produces a new reply and a new charge. If it persists, send us the `request_id`.
  </Accordion>
</AccordionGroup>

## A note on 401

The API does not distinguish a deleted key from one that never existed: both are `invalid_api_key`. If a key that used to work starts returning 401, check the API access card in the app; it was probably rotated or deleted.
