# Errors

> Every error the Kabu API returns is a problem with a stable code. What the fields mean, how to handle them, and every code with its status.

When a request fails, the API answers with `application/problem+json` (RFC 9457): one shape for every error, with a
stable `code` that has its own page here. The response's `doc_url` is that page.

```json
{
  "type": "https://usekabu.com/docs/errors/invalid_request",
  "title": "Invalid request",
  "status": 400,
  "code": "invalid_request",
  "detail": "The body does not match the contract.",
  "hint": "Fix the fields listed in errors and resend.",
  "param": "/title",
  "errors": [{ "param": "/title", "code": "invalid_request", "detail": "…" }],
  "retryable": false,
  "request_id": "req_01J9Z…",
  "doc_url": "https://usekabu.com/docs/errors/invalid_request"
}
```

| Field | What it is |
|---|---|
| `code` | Stable, machine-readable: branch on it |
| `status` | The HTTP status, also the response's |
| `detail` | What went wrong with this request, for people |
| `hint` | The next step, in one sentence an agent can act on |
| `param` | The field at fault, as a JSON pointer, such as `/ask/options/2/label` |
| `errors` | When several fields fail: one `{param, code, detail}` each |
| `retryable` | Whether the same request may succeed later unchanged |
| `request_id` | Quote it to [support](/support) |

## Handling errors

- **Branch on `code`**, and on `status` for a code you do not know: new codes can appear.
- **Retry only what can succeed:** `rate_limited` after its `Retry-After`, `idempotency_key_in_use` after a moment, and
  a `5xx` with backoff. Send retries with the same `Idempotency-Key`, so nothing happens twice.
- **Never retry a `4xx` unchanged** otherwise: fix the request, using `hint` and `param`.

## Every code

| Code | Status | Means |
|---|---|---|
| [`invalid_request`](/docs/errors/invalid_request) | `400` | A request or a value in it breaks one of the contract's rules: a missing field, a value too long, an option that does not exist. |
| [`unknown_parameter`](/docs/errors/unknown_parameter) | `400` | The request has a field or query parameter the contract does not define. Request bodies are closed, so a typo never passes silently. |
| [`unauthorized`](/docs/errors/unauthorized) | `401` | No valid credential came with the request: the key is missing, mistyped or revoked, or a signed-in session has expired. |
| [`forbidden`](/docs/errors/forbidden) | `403` | The credential is valid but may not do this: the key lacks a scope, is bound to another project, or the operation takes another kind of credential. |
| [`self_response_forbidden`](/docs/errors/self_response_forbidden) | `403` | A key that sends messages tried to act as a person: to answer an ask, mint an answer link or pair a recipient. No agent may answer itself. |
| [`incompatible_scopes`](/docs/errors/incompatible_scopes) | `400` | A key was to hold messages:write together with answers:delegate or steer:write. Asking and answering never share a key. |
| [`not_found`](/docs/errors/not_found) | `404` | No such resource, or none this key may see: Kabu answers the same for both, so ids in other accounts stay secret. |
| [`idempotency_key_reused`](/docs/errors/idempotency_key_reused) | `422` | The Idempotency-Key was used before with a different request body. A key names one request; a different request needs a new key. |
| [`idempotency_key_in_use`](/docs/errors/idempotency_key_in_use) | `409` | A request with the same Idempotency-Key is still being handled. Wait a moment, then send the same request again. |
| [`already_settled`](/docs/errors/already_settled) | `409` | The ask was already answered, expired or was cancelled, so it cannot be answered again. The response includes the settled message. |
| [`ask_expired`](/docs/errors/ask_expired) | `410` | The ask's deadline has passed, so it can no longer be answered. |
| [`lease_lost`](/docs/errors/lease_lost) | `409` | Steering, a preview: the lease on a claimed steering instruction is no longer current, so it cannot be acknowledged with that token. |
| [`invalid_ack_transition`](/docs/errors/invalid_ack_transition) | `409` | Steering, a preview: an acknowledgement would move a steering instruction to a state it cannot reach from its current one. |
| [`rate_limited`](/docs/errors/rate_limited) | `429` | Too many requests in a short time, or the agent is paused for sending near-identical messages. Wait for Retry-After, then send the same request. |
| [`quota_exceeded`](/docs/errors/quota_exceeded) | `429` | The account's monthly notifications are used up, or it reached the cap on projects or agents created by name. Retrying will not help. |
| [`payload_too_large`](/docs/errors/payload_too_large) | `413` | The request body or file is too big: JSON bodies are at most 64 KiB, and a direct upload at most 25 MiB. |
| [`unsupported_media_type`](/docs/errors/unsupported_media_type) | `415` | The request's Content-Type is not one this operation takes: JSON for most, multipart/form-data for a direct upload. |
| [`checkpoint_expired`](/docs/errors/checkpoint_expired) | `410` | The events checkpoint is older than events are kept. Start again from a new checkpoint, the current state, then newer events. |
| [`checkpoint_filter_mismatch`](/docs/errors/checkpoint_filter_mismatch) | `400` | The events checkpoint was issued for other filters. A checkpoint resumes only the reading it came from. |
| [`upload_expired`](/docs/errors/upload_expired) | `410` | The presigned upload was not finished in time; its bytes never became media. Create a new upload. |
| [`media_not_ready`](/docs/errors/media_not_ready) | `400` | A message attaches media that is not ready: still uploading, failed, or from another project. |
| [`unsupported_schema_keyword`](/docs/errors/unsupported_schema_keyword) | `400` | The action's args\_schema uses a JSON Schema keyword outside the supported set, such as format or $dynamicRef. |
| [`invalid_receipt`](/docs/errors/invalid_receipt) | `422` | The receipt does not verify: its signature, issuer, org or mode does not match. Send it exactly as Kabu returned it. |
| [`attempt_expired`](/docs/errors/attempt_expired) | `409` | Steering, a preview: the window to report the outcome of a steering instruction has closed. |
| [`precondition_failed`](/docs/errors/precondition_failed) | `412` | The resource changed since you read it (If-Match), or an upload is already finalised and cannot change. |
| [`session_ended`](/docs/errors/session_ended) | `409` | The session has ended, so it takes no new messages or answers. Start a new session. |
| [`project_in_use`](/docs/errors/project_in_use) | `409` | A project cannot be deleted while an API key is bound to it or to one of its agents. |
| [`not_implemented`](/docs/errors/not_implemented) | `501` | The operation is in the contract but not available on this server yet. |
| [`internal_error`](/docs/errors/internal_error) | `500` | Something failed on Kabu's side. Retry with the same Idempotency-Key; if it persists, tell support with the request\_id. |
