# Errors

> The error envelope, error types and HTTP statuses, retry guidance, and every error code the K-Agent API returns — what it means and what to do.

Every error from the K-Agent API has the same shape, a stable `code` you can branch on, and a message written for people in Arabic or English.

## The error envelope

```json
{
  "error": {
    "type": "conflict_error",
    "code": "session_busy",
    "message": "This session is answering another message. Retry in a moment.",
    "request_id": "req_01k6rz9f1j3m5p7r9t1w3y5a7c",
    "doc_url": "https://k-agent.kerneltics.com/docs/en/reference/errors/#session_busy"
  }
}
```

| Field | Description |
|---|---|
| `type` | The broad category, below. |
| `code` | Stable, `snake_case`. **Branch on this.** New codes may be added; treat unknown ones by their `type` and HTTP status. |
| `message` | For people, in the language of `Accept-Language` (`ar` or `en`). It can change at any time — never parse it. |
| `param` | Present when one parameter is at fault: a field name, or a JSON pointer such as `/tools/max_tool_rounds`. |
| `request_id` | The same value as the `Request-Id` response header. Quote it when you contact support. |
| `doc_url` | A link to this page, at the code's row. |

## Types and statuses

| `type` | HTTP status | Meaning |
|---|---|---|
| `invalid_request_error` | 400 (malformed), 422 (understood but invalid), plus 405, 413 and 428 | Fix the request. |
| `authentication_error` | 401 | The credential is missing, invalid, expired or revoked. |
| `permission_error` | 403 | The credential is valid but may not do this. |
| `not_found_error` | 404 | It doesn't exist — or it belongs to another project or another end user. |
| `conflict_error` | 409, or 412 for a stale `If-Match` | The current state doesn't allow it. |
| `rate_limit_error` | 429 | Too many requests; wait for `Retry-After`. |
| `quota_error` | 429 | A plan or cost limit was reached. |
| `provider_error` | 502 | The AI provider failed. |
| `api_error` | 500, 503 | Something went wrong on our side. |

## Retrying safely

- **Retry** `429` after the `Retry-After` seconds, `409 session_busy`, `session_queue_full` and `idempotency_in_progress` after a short pause, and `500`, `502` and `503` with exponential backoff. Codes marked *Retryable* below are the ones worth retrying.
- **Don't retry** when the response has `x-should-retry: false` (for example `quota_exceeded`), or for other `4xx` errors — fix the request first.
- **Always send an `Idempotency-Key`** on POST requests you might retry, so a retry can never create a second message, session or ticket. See [Idempotency](/docs/en/reference/idempotency/).

## Errors in streams

Errors that happen before a stream starts are normal JSON responses. After it starts, a failed run ends with a `run.failed` event that carries `run.error`, and the `error` event is used only for stream faults (`token_expired`, `stream_timeout`, `internal_error`). See [Runs and streaming](/docs/en/concepts/runs-and-streaming/#errors-and-edge-cases).

## Error codes

### Invalid request (400, 405, 413, 422, 428)

| Code | HTTP | Meaning | What to do |
|---|---|---|---|
| `allowed_origins_required` | 422 | A publishable key needs at least one allowed origin, such as https://www.example.com. | Add at least one origin (`scheme://host[:port]`) to the publishable key's `allowed_origins`. |
| `credential_invalid` | 422 | The AI provider rejected this API key. Check the key and try again. | The provider rejected the key during the live check. Copy the key again from the provider's console and make sure it can call the chosen model. |
| `draft_not_allowed` | 422 | The draft can only run in playground sessions. | `version: "draft"` runs only in playground sessions (`channel: "playground"`), for editors or keys with `agents:write`. Publish to use the changes anywhere else. |
| `external_id_invalid` | 422 | The external_id format is invalid. Use 1 to 128 Latin letters, digits or the characters . _ : -, starting with a letter or digit. Session IDs must not start with sess_ and end-user IDs must not start with eu_. | Use 1–128 characters from `A–Z a–z 0–9 . _ : -`, starting with a letter or digit. Session IDs must not start with `sess_`, end-user IDs must not start with `eu_`. |
| `idempotency_key_reused` | 422 | This Idempotency-Key was already used with a different request. Use a new key for each new request. | Use a new `Idempotency-Key` for each distinct request. Reuse a key only to retry the exact same request. |
| `identity_as_parameter` | 422 | Tools that require a verified customer must not ask the model for identity details such as phone numbers or emails. Use end_user placeholders instead. | Remove phone, email or user-ID parameters from tools that need a verified user, and use `{{end_user.external_id}}` or `{{end_user.traits.*}}` placeholders instead. |
| `if_match_required` | 428 | This change needs an If-Match header with the current etag. | Send `If-Match: <etag>` with the draft's current etag when you `PATCH` the agent configuration. |
| `input_too_large` | 413 | The input is too long. | Shorten the input: up to 4,000 characters with browser credentials, 32,000 with a secret key. |
| `invalid_api_version` | 400 | The K-Agent-Version header names an unknown API version. | Send `K-Agent-Version: 2026-10-06`, or leave the header out. |
| `invalid_cursor` | 400 | The pagination cursor is not valid for this list. | Use `first_id` or `last_id` from a previous page of the same list, and don't send `after` and `before` together. |
| `invalid_idempotency_key` | 400 | The Idempotency-Key header must be 1 to 255 characters long. | Keep `Idempotency-Key` between 1 and 255 characters; a UUID works well. |
| `invalid_json` | 400 | The request body is not valid JSON. | Send a valid JSON body. Check quotes and trailing commas. |
| `invalid_parameter` | 400 | The parameter "{param}" has an invalid value. | Fix the value named in `param`; the API reference lists the allowed values. |
| `method_not_allowed` | 405 | This endpoint doesn't support this HTTP method. | Use the HTTP method the API reference shows for this path. |
| `model_not_allowed_on_plan` | 422 | This model isn't available on your current plan. | Pick a model your plan allows (`GET /v1/models` shows `allowed_on_plan`) or upgrade the plan. |
| `override_not_allowed` | 422 | This agent doesn't allow changing this setting per request. | Add the field to the agent's `overrides.allowed` (and the model to `overrides.models`), publish, then retry. `never_handle`, `escalation_reply` and tool grants can never be overridden. |
| `placeholder_unresolved` | 422 | A placeholder in the tool's URL, headers or body had no value, so the call was not made. | A `{{…}}` placeholder in the tool's URL, headers or body had no value, so nothing was sent. Make the parameter required, pass the variable, or check the end user's traits. |
| `project_mismatch` | 400 | The X-Project-Id header names a different project from the one this key or token belongs to. | API keys and tokens already belong to a project. Drop the `X-Project-Id` header, or use a key from that project. |
| `project_required` | 400 | Choose a project by sending its ID in the X-Project-Id header. | Dashboard cookie requests must send `X-Project-Id`. With an API key, the project comes from the key. |
| `reference_not_found` | 422 | The configuration refers to an item that doesn't exist in this project. | The configuration points at an ID (`tool_…`, `ks_…`, `pcred_…`) that doesn't exist in this project. Create the item first or fix the reference. |
| `request_too_large` | 413 | The request body is too large. | Keep request bodies under 1 MiB (5 MiB for knowledge sources). |
| `secret_host_not_allowed` | 422 | A secret can only be sent to the hosts listed in its allowed_hosts. | Add the tool's host to the secret's `allowed_hosts`, or use a separate secret for that host. |
| `secret_variable_not_persistable` | 422 | Secret variables can only be sent with a single message or ask request; they are never stored on a session. | Send secret variables with each request (`ask` or `messages`), never on `POST /v1/sessions` or `PATCH`. |
| `system_message_not_allowed` | 400 | System and developer messages aren't accepted; the agent's own instructions apply. | Remove `system` and `developer` messages and put instructions in the agent. If the agent allows the `instructions_append` override, they are used as per-request instructions. |
| `tool_name_conflict` | 422 | Another tool of this agent already uses this name. | Rename the tool: names must be unique across the agent's built-in, HTTP and client tools. |
| `tool_not_declared` | 400 | The request includes a tool that the agent doesn't declare as a client-side tool. | Only send `tools` that the agent declares as client tools, with the same names. |
| `tool_outputs_incomplete` | 422 | Send an output for every pending tool call. | Send one output for every `call_id` in `required_action.tool_calls`. |
| `tools_not_allowed_with_session` | 400 | Client-side tools can't be sent when the request is bound to a session. | Drop `tools` from OpenAI-compatible requests bound to a session, or call without a session. |
| `unknown_event_type` | 422 | The event list contains a type that is not in the event catalog. Use catalog event types or ["*"]. | Subscribe only to event types from the catalog in the webhooks guide, or to `["*"]` for all of them. |
| `unknown_field` | 400 | The field "{param}" is not recognized. | Remove the field named in `param`. Unknown fields are rejected so that typos never pass silently. |
| `unknown_model` | 422 | This model is not known. | Use a model ID from `GET /v1/models`. |
| `unsupported_content_type` | 422 | Only text content is supported. | Send text only: `input` as a string or as `[{"type":"text","text":"…"}]`. |
| `unsupported_parameter` | 400 | The request uses a parameter that isn't supported. | Remove `n` greater than 1, `logprobs` or `response_format: json_schema` from OpenAI-compatible requests. |
| `url_not_allowed` | 422 | This URL is not allowed. Use a public https:// address. | Use a public `https://` URL on port 443 or 8443. Private, loopback and cloud-metadata addresses are blocked. |
| `validation_failed` | 422 | The request contains an invalid value. | Fix the value at the JSON pointer in `param`; the message says what is wrong. |
| `variable_missing` | 422 | A required variable was not provided. | Send every variable the agent marks `required`. |
| `variable_unknown` | 422 | The request sets a variable that this agent doesn't declare. | Send only variables the agent declares, and check their spelling. |
| `webhook_endpoint_limit_reached` | 422 | This project already has the maximum number of webhook endpoints. | A project can have up to 10 webhook endpoints. Delete one, or subscribe an existing endpoint to more events. |

### Authentication (401)

| Code | HTTP | Meaning | What to do |
|---|---|---|---|
| `authentication_required` | 401 | Authentication is required. Send your API key in the Authorization header as "Bearer <key>". | Send `Authorization: Bearer <key>` with a secret key, a publishable key or a client token. |
| `invalid_api_key` | 401 | The API key is invalid, expired or revoked. | The key is mistyped, expired, rolled or revoked. Create or roll a key in the dashboard. |
| `invalid_client_token` | 401 | The client token is invalid. | Mint a new client token on your server with `POST /v1/client_tokens`. |
| `invalid_credentials` | 401 | The email or password is incorrect. | Check the email and password. Repeated failures are rate limited. |
| `token_expired` | 401 | The client token has expired. Request a new one. | Get a new client token (widget: `POST /v1/widget/token/refresh`; your server: `POST /v1/client_tokens`) and reconnect streams with `Last-Event-ID`. Open streams receive this code as an `error` event. |

### Permission (403)

| Code | HTTP | Meaning | What to do |
|---|---|---|---|
| `csrf_check_failed` | 403 | This request was blocked because it came from another site. | Dashboard cookie requests must be same-origin JSON requests. Server integrations should use an API key, not cookies. |
| `insufficient_scope` | 403 | This API key doesn't have the scopes this request needs. | Use a key that has the scope this route needs (for example `runs:write` or `sessions:read`), or a key with `permissions: "all"`. |
| `origin_not_allowed` | 403 | This website's origin is not allowed for this key. Add it to the key's allowed origins. | Add this exact origin (`scheme://host[:port]`) to the publishable key's `allowed_origins`. |
| `origin_required` | 403 | Requests with a publishable key must come from a browser that sends an Origin header. | Publishable keys only work from browsers, which send `Origin`. From a server, use a secret key. |
| `permission_denied` | 403 | You don't have permission to do this. Required permission: {permission}. | Your role lacks the permission named in the message. Ask an admin for a role that includes it. |
| `principal_not_allowed` | 403 | This kind of credential can't call this endpoint. | This route needs a secret key or a dashboard user. Publishable keys and client tokens can call only the browser routes listed under End users & identity. |
| `signup_closed` | 403 | Sign-up is closed on this server. Ask an administrator for an invitation. | Ask an administrator for an invitation. |

### Not found (404)

| Code | HTTP | Meaning | What to do |
|---|---|---|---|
| `project_not_found` | 404 | Project not found, or you are not a member of it. Check the X-Project-Id header. | Check the `X-Project-Id` header: the project doesn't exist, or you aren't a member of its organization. |
| `resource_not_found` | 404 | The requested {resource} was not found. | Check the ID or slug. Items in another project, or owned by another end user, also return 404. |
| `route_not_found` | 404 | There is no endpoint at this path. Check the URL against the API reference. | Check the path, including the `/v1` prefix, against the API reference. |
| `session_not_found` | 404 | Session not found. Create sessions with POST /v1/sessions; you can then use either its ID or your external_id. | Create sessions with `POST /v1/sessions` (get-or-create), then address them by `sess_…` ID or by your `external_id`. |

### Conflict (409, 412)

| Code | HTTP | Meaning | What to do |
|---|---|---|---|
| `agent_archived` | 409 | This agent is archived and no longer answers new messages. | The agent was archived (`DELETE /v1/agents/{agent}`). Use another agent, or create a new one from its export. |
| `agent_not_ready` | 409 | The agent isn't ready to answer yet. Check the blockers listed in its readiness. | Read the agent's `readiness.blockers` (`GET /v1/agents/{agent}`): connect a provider key, pick a model your plan allows or turn off `ai_paused`. Retrying won't help until a blocker is fixed. |
| `already_member` | 409 | This person is already a member of the organization. | The person is already in the organization; change their role instead. |
| `client_message_id_conflict` | 409 | This client_message_id was already used for a different message. | You reused a `client_message_id` with different content. Generate a new ID for every new message; reuse an ID only to retry the same message. |
| `email_taken` | 409 | An account with this email already exists. Log in instead. | Log in instead, or sign up with another email. |
| `etag_mismatch` | 412 | This item changed since you loaded it. Reload it and apply your changes again. | The draft changed after you read it. Fetch the agent again, re-apply your change and send the new `etag` in `If-Match`. |
| `handoff_already_assigned` | 409 | Another team member has already claimed this handoff. | Another teammate claimed it first. Refresh the queue; admins can reassign with `force: true`. |
| `handoff_already_open` | 409 | A handoff is already open for this session. | This session already has an open handoff. Work it in the Handoff Desk, or resolve it before requesting another. |
| `handoff_not_open` | 409 | This handoff is no longer open. | The handoff was already resolved or expired. Refresh it before acting. |
| `idempotency_in_progress` | 409 | A request with this Idempotency-Key is still being processed. Retry after it finishes. | The first request with this key is still running. Retry with backoff; once it finishes you get its stored result. |
| `invitation_expired` | 409 | This invitation has expired or was revoked. Ask for a new one. | Ask an admin for a new invitation link. Links work once and expire after 7 days. |
| `last_owner` | 409 | An organization must keep at least one owner. | Make another member an owner before removing or demoting this one. |
| `model_not_configured` | 409 | No API key is configured for this model's provider. Add a provider key in Settings. | Connect a key for the model's provider (Settings → Model providers, or `POST /v1/provider_credentials`), or pick a model the platform provides. |
| `name_taken` | 409 | This name is already in use. Choose a different one. | Pick a different name or slug. |
| `no_open_handoff` | 409 | This session has no open handoff. | There is no open handoff to release or resolve. Fetch the session to check its `mode`. |
| `run_already_completed` | 409 | This run has already finished and can't be cancelled. | The run already finished. Nothing to do. |
| `run_not_requires_action` | 409 | This run isn't waiting for tool outputs. | Submit tool outputs only while the run's status is `requires_action`. Fetch the run to see its state. |
| `session_agent_mismatch` | 409 | This external_id already belongs to a session with a different agent. | This `external_id` already belongs to a session with another agent. Use one ID per agent, for example by prefixing the agent slug. |
| `session_busy` | 409 | This session is answering another message. Retry in a moment. | The session is answering another message and uses `concurrency: "reject"`. Wait `Retry-After` seconds, or switch the session to `queue`. |
| `session_closed` | 409 | This session is closed. | You sent `if_closed: "error"`. Leave it at the default `reopen` to reopen the session, or start a new one. |
| `session_end_user_mismatch` | 409 | This session belongs to a different end user. | The session belongs to another end user, and a session's end user never changes. Use a different session ID. |
| `session_exists` | 409 | A session with this external_id already exists. | You sent `if_exists: "error"` and the session exists. Drop the flag to resume it, or use a new `external_id`. |
| `session_queue_full` | 409 | Too many messages are waiting in this session. Wait for the agent to reply, then try again. | Ten messages are already waiting in this session. Wait for replies before sending more. |
| `slug_taken` | 409 | Another agent in this project already uses this slug. | Another agent in this project uses this slug. Choose a different `slug`. |

### Rate limits and quota (429)

| Code | HTTP | Meaning | What to do |
|---|---|---|---|
| `anonymous_limit_reached` | 429 | Too many messages right now. Please try again later. | An anonymous widget visitor hit a per-IP, per-session or daily cap. Ask them to try later, or identify signed-in users with client tokens. |
| `cost_cap_exceeded` | 429 | The daily usage cap has been reached. Please try again tomorrow. | The organization reached its daily model-cost cap. Sessions hand off and `ask` returns 429 until the next day; contact us to raise the cap. |
| `playground_limit_reached` | 429 | The daily limit for test conversations has been reached. Connect your own provider key to keep testing. | Test-panel runs on platform models are capped per project per day. Connect your own provider key to keep testing, or try tomorrow. |
| `quota_exceeded` | 429 | Your plan's AI conversation quota for this month is used up. | The plan's AI conversations for this month are used up (Free, or a paid plan with a hard cap). Upgrade or wait for next month; the response carries `x-should-retry: false`. |
| `rate_limited` | 429 | Too many requests. Wait a moment and try again. | Wait the number of seconds in `Retry-After`, then retry, and spread requests out. |

### AI provider (502)

| Code | HTTP | Meaning | What to do |
|---|---|---|---|
| `provider_auth_failed` | 502 | The AI provider rejected the credentials. | The model provider rejected the credentials. Check or replace the provider key. |
| `provider_bad_request` | 502 | The AI provider rejected the request. | The provider rejected the request. Check the model settings; contact support with the `request_id` if it persists. |
| `provider_error` | 502 | The AI provider returned an error. | Retry later. Configure `model.fallback_models` so runs continue on another model. |
| `provider_overloaded` | 502 | The AI provider is overloaded. Try again shortly. | Retry with backoff; `model.fallback_models` keeps conversations going meanwhile. |
| `provider_rate_limited` | 502 | The AI provider is limiting requests. Try again shortly. | The provider is rate limiting the key. Retry with backoff or raise your limits with the provider. |
| `provider_refusal` | 502 | The AI model declined to answer. | The model declined to answer. K-Agent tries `fallback_models`, then sends the fallback message and hands sessions to staff. |
| `provider_timeout` | 502 | The AI provider took too long to respond. | Retry. If it keeps happening, choose a faster model or a smaller `max_reply_tokens`. |
| `provider_unavailable` | 502 | The AI provider is unavailable right now. | Retry later, and configure `model.fallback_models`. |

### Server (500, 503)

| Code | HTTP | Meaning | What to do |
|---|---|---|---|
| `internal_error` | 500 | Something went wrong on our side. Please try again; if it keeps happening, contact support with the request ID. | Retry with backoff. If it keeps happening, contact support with the `request_id`. |
| `service_unavailable` | 503 | The service is temporarily unavailable. Try again shortly. | The server is restarting or overloaded. Retry with backoff. |

### Codes outside HTTP responses

These codes never come back as an HTTP error. They appear in a run's `error` (run errors), as a stream `error` event, or in a tool result that only the model sees — you will find them in run steps and the dashboard's Debug tab.

| Code | HTTP | Meaning | What to do |
|---|---|---|---|
| `handed_off` | Tool result | The conversation was handed to a member of staff. | Returned to the model for the remaining tool calls of a round in which a liability handoff ended the turn. Nothing to do. |
| `no_model_credential` | Run error | No API key is connected for the agent's AI provider. | The agent's AI provider has no API key, so the run ended with the fallback message. Connect a provider key (Settings → Model providers, or `POST /v1/provider_credentials`). API calls get `409 agent_not_ready` with this blocker instead. |
| `run_interrupted` | Run error | The run was interrupted before it finished. | The run stopped before finishing (for example during a server restart) and ended through the never-silent path. Check the session and send the message again if needed. |
| `stream_timeout` | Stream event | The stream was open too long and has been closed. Reconnect with Last-Event-ID to continue. | Reconnect with `Last-Event-ID` (or `?after=`) to continue from the last event you received. |
| `ticket_already_open` | Tool result | This customer already has an open ticket. | Returned to the model when the customer already has an open ticket; it tells them the existing ticket number. Nothing to do. |
| `too_many_calls` | Tool result | Too many tool calls in one step. | At most 5 tool calls run per round; the model receives this for the extra calls. Nothing to do. |
| `tool_outputs_expired` | Run error | The run waited too long for tool outputs and was stopped. | Tool outputs must arrive within 10 minutes of `requires_action`. The run is failed; send a new message to continue. |
| `unreadable` | Tool result | The lookup came back in a form that could not be read. | Your HTTP tool's endpoint returned something that isn't JSON or doesn't match `response.items_path` and `fields`. Fix the endpoint or the projection, and check it with `POST /v1/tools/{tool}/test`. |
| `upstream_failed` | Tool result | The lookup could not be completed right now. | Your HTTP tool's endpoint failed (non-2xx, timeout, over 1 MiB or a blocked address). Inspect the run steps and test the tool. |

## Warnings

Some successful responses carry `warnings`: non-fatal notes worth logging.

| Warning | Meaning |
|---|---|
| `external_id_looks_like_phone` | The `external_id` looks like a phone number. Keep personal data out of IDs; store contact details in end-user traits. |
| `external_id_looks_like_email` | The `external_id` looks like an email address. Same advice. |
| `create_params_ignored` | The session already existed, so creation-only settings in the request were ignored. |
| `client_history_ignored` | OpenAI-compatible endpoint: the session's stored history was used and earlier messages in the request were ignored. |
| `identity_as_parameter` | A tool parameter is named like personal identity data. Turn on `requires_verified_user` and use `end_user` placeholders instead. |
