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

# Errors

> Every error Kyma returns, what it means, and what to do next.

Kyma answers a failed request with a JSON error body and an HTTP status that describes your request, not the model behind it. Read `error.code` to decide what to do: it is stable, while `error.message` is written for people and can change. `error.retryable` (also sent as the `x-should-retry` header) says whether sending the same request again can succeed. `error.request_id` identifies the call; include it when you contact us.

## The error object

On OpenAI-compatible endpoints (`/v1/chat/completions`, `/v1/completions`, `/v1/embeddings` and the rest):

```json theme={null}
{
  "error": {
    "message": "This request needs about $0.58 of credit; you have $0.39. Top up at https://kymaapi.com/billing or lower max_tokens.",
    "type": "billing_error",
    "code": "insufficient_credits",
    "param": null,
    "retryable": false,
    "request_id": "req_7f3c2a9e0b1d4c6f8a2e5b7d9c1f3a5e",
    "doc_url": "https://docs.kymaapi.com/api-reference/errors#insufficient_credits"
  },
  "need_usd": 0.58,
  "have_usd": 0.39,
  "topup_url": "https://kymaapi.com/billing",
  "topup": "https://api.kymaapi.com/v1/topup"
}
```

| Field | Type | Meaning |
| - | - | - |
| `message` | string | What happened and what to do next, for a person. Do not parse it. |
| `type` | string | The category, one of the [error types](#error-types). |
| `code` | string | The machine code, one of the [error codes](#error-codes). Always a string. |
| `param` | string or null | The request field at fault, when there is one. |
| `retryable` | boolean | `true` when the same request can succeed if sent again. |
| `request_id` | string | Same value as the `x-request-id` header. |
| `doc_url` | string | This code's entry on this page. |

Some errors add fields next to `error`: `need_usd`, `have_usd`, `topup_url` and `topup` on a 402, `did_you_mean` and `suggested_models` on an unknown model. On `route_refused`, `suggested_models` is inside `error`.

## On `/v1/messages`

`/v1/messages` answers in Anthropic's error envelope, so Anthropic SDKs and Claude Code handle Kyma errors unchanged. `error.type` is an Anthropic error type, the Kyma `code` sits next to it, and `request_id` is also at the top level:

```json theme={null}
{
  "type": "error",
  "error": {
    "type": "authentication_error",
    "code": "missing_api_key",
    "message": "Auth required",
    "param": null,
    "retryable": false,
    "request_id": "req_480c522e74474c2fb72d9c4a8aa05151",
    "doc_url": "https://docs.kymaapi.com/api-reference/errors#missing_api_key"
  },
  "request_id": "req_480c522e74474c2fb72d9c4a8aa05151"
}
```

The `upstream_error` type becomes `overloaded_error` on a 503 and `api_error` on a 502.

## Media jobs and kyma-gen

A failed image or audio job still answers `GET /v1/jobs/{id}` with HTTP 200; the job carries `status: "failed"` and `error: { code, message }`. Job codes describe what happened to the job: `content_policy`, `content_policy_likeness`, `content_policy_nsfw`, `content_policy_unsafe`, `moderation_blocked`, `input_too_large`, `input_invalid`, `invalid_input`, `prompt_too_long`, `provider_timeout`, `timeout`, `provider_rate_limited`, `provider_unavailable`, `insufficient_credits`. A failed job is refunded.

`/v1/genmedia/*` (used by the kyma-gen CLI) keeps its own shape: `{ "error": "<message>", "details": { "error_type": "...", ... } }`.

## Request IDs

Every response carries an `x-request-id` header, errors and successes alike, and every error body repeats it as `error.request_id`. Include it when you email [hello@kymaapi.com](mailto:hello@kymaapi.com); it lets us find the exact call.

<CodeGroup>
  ```bash curl theme={null}
  curl -si https://api.kymaapi.com/v1/chat/completions \
    -H "Authorization: Bearer $KYMA_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"model":"qwen-3.6-plus","messages":[{"role":"user","content":"hi"}]}' \
    | grep -i x-request-id
  ```

  ```python Python theme={null}
  from openai import OpenAI, APIStatusError

  client = OpenAI(base_url="https://api.kymaapi.com/v1", api_key="YOUR_KEY")
  try:
      client.chat.completions.create(model="qwen-3.6-plus", messages=[{"role": "user", "content": "hi"}])
  except APIStatusError as e:
      # The SDK reads x-request-id for you.
      print(e.status_code, e.code, e.request_id)
  ```

  ```javascript JavaScript theme={null}
  import OpenAI from "openai";

  const client = new OpenAI({ baseURL: "https://api.kymaapi.com/v1", apiKey: "YOUR_KEY" });
  try {
    await client.chat.completions.create({ model: "qwen-3.6-plus", messages: [{ role: "user", content: "hi" }] });
  } catch (e) {
    // The SDK reads x-request-id for you.
    if (e instanceof OpenAI.APIError) console.log(e.status, e.code, e.request_id);
  }
  ```
</CodeGroup>

## Should I retry?

1. Read `x-should-retry` or `error.retryable` first. The official OpenAI and Anthropic SDKs already honor `x-should-retry` and retry twice by default.
2. If it is retryable, wait `Retry-After` seconds when the header is present, then retry with exponential backoff and jitter.
3. Statuses that can be retried: 408, 429, 502, 503, 504, and a stream that ended with `stream_interrupted`.
4. Do not retry 400, 401, 402, 403, 404 or 413 unchanged: they fail the same way until you change the request, the key or the balance.

## Error types

| Type | HTTP | Meaning |
| - | - | - |
| `invalid_request_error` | 400, 405, 409, 415, 422 | The request itself is wrong; change it. |
| `authentication_error` | 401 | No key, or the key is not valid. |
| `billing_error` | 402 | Money or free-tier access; add credits. |
| `permission_error` | 403 | Valid key, but not allowed for this resource or content. |
| `not_found_error` | 404, 410 | The endpoint or model does not exist, or was retired. |
| `request_too_large` | 413 | Too big for the endpoint. |
| `rate_limit_error` | 429 | Too fast or too many at once; wait. |
| `timeout_error` | 408, 504 | Something took too long; retry. |
| `upstream_error` | 502, 503, in a stream | The model's routes failed; not your request. |
| `api_error` | 500, 503 | Kyma failed; retry, and quote `request_id` if it repeats. |

## Error codes

Codes are stable: new ones are added, existing ones are never renamed or given a new meaning. Codes marked **Not returned yet** are reserved; endpoints send the older value listed under [Older values](#older-values) until then. Some image, audio, embedding and rerank errors still use older values too.

### Authentication and access

| Code | HTTP | Type | What happened | What to do | Retry |
| - | - | - | - | - | - |
| <span id="missing_api_key">`missing_api_key`</span> | 401 | `authentication_error` | No API key was sent. | Send `Authorization: Bearer <key>` (or `x-api-key` on `/v1/messages`). | No |
| <span id="invalid_api_key">`invalid_api_key`</span> | 401 | `authentication_error` | The key is unknown, revoked or disabled, or the session expired. | Create a key at kymaapi.com/keys and use it instead. | No |
| <span id="permission_denied">`permission_denied`</span> | 403 | `permission_error` | The key is valid but cannot use this endpoint. | Use an endpoint your account can call. | No |
| <span id="session_required">`session_required`</span> | 403 | `permission_error` | This endpoint changes the account (API keys, billing settings) and takes a dashboard session, not an API key. | Sign in at kymaapi.com and do it from the dashboard. | No |
| <span id="csrf_failed">`csrf_failed`</span> | 403 | `csrf_failed` today | A dashboard cookie request arrived without its CSRF header. **Not on the contract yet:** today's body has `error.type: "csrf_failed"`, `error.code: 403`, and no `retryable` field or `x-should-retry` header. | Send `X-Kyma-CSRF`, or call with an API key. | No (no `retryable` field yet) |

### The request

| Code | HTTP | Type | What happened | What to do | Retry |
| - | - | - | - | - | - |
| <span id="voice_not_owned">`voice_not_owned`</span> | 403 | `forbidden` today | The `voice_id` belongs to another account. **Not on the contract yet:** today's body has `error.type: "forbidden"`, `error.code: "voice_not_owned"`, and no `retryable` field or `x-should-retry` header. | Use a voice from `GET /v1/audio/voices`. | No (no `retryable` field yet) |
| <span id="content_refused">`content_refused`</span> | 400 | `invalid_request_error` | The model's safety filter refused this prompt (media generation). Nothing was charged. | Rephrase the prompt; for music, do not name a specific artist's voice or quote copyrighted lyrics. | No |
| <span id="invalid_json">`invalid_json`</span> | 400 | `invalid_request_error` | The body is not valid JSON. | Send valid JSON with `Content-Type: application/json`. | No |
| <span id="missing_parameter">`missing_parameter`</span> | 400 | `invalid_request_error` | A required field is missing; `param` names it. | Add the field. | No |
| <span id="invalid_parameter">`invalid_parameter`</span> | 400 | `invalid_request_error` | A field is present but its value is not accepted. | Fix the value as `error.message` says. | No |
| <span id="unsupported_content">`unsupported_content`</span> | 400 | `invalid_request_error` | A content block this model cannot read, such as an image or a non-text document. | Remove the block, or pick a model from `GET /v1/models?input_modalities=image`. | No |
| <span id="unsupported_media_type">`unsupported_media_type`</span> | 415 | `invalid_request_error` | **Not returned yet.** The uploaded file type is not accepted. | Convert the file to a listed format. | No |
| <span id="request_rejected">`request_rejected`</span> | 400 | `invalid_request_error` | The model rejected a parameter value (the status is 400, 413 or 422). | Change the parameters `error.message` names. Retrying unchanged fails the same way. | No |
| <span id="no_speech_detected">`no_speech_detected`</span> | 422 | `invalid_request` today | The audio has no speech in it. **Not on the contract yet:** today's body has `error.type: "invalid_request"`, `error.code: "no_speech_detected"`, and no `retryable` field or `x-should-retry` header. | Check the file. Nothing was charged. | No (no `retryable` field yet) |
| <span id="segments_unsupported">`segments_unsupported`</span> | 400 | `invalid_request` today | This transcription model cannot return timestamps. **Not on the contract yet:** today's body has `error.type: "invalid_request"`, `error.code: "segments_unsupported"`, and no `retryable` field or `x-should-retry` header. | Use `response_format=json` or `text`, or a model from `suggested_models`. | No (no `retryable` field yet) |
| <span id="method_not_allowed">`method_not_allowed`</span> | 405 | `invalid_request_error` | Wrong HTTP method for this path. | Use the method the endpoint page shows. | No |
| <span id="request_too_large">`request_too_large`</span> | 413 | `request_too_large` | The body is over the endpoint limit (25 MB on chat). | Split the request, downscale images, or send a URL. | No |
| <span id="endpoint_not_found">`endpoint_not_found`</span> | 404 | `not_found_error` | No such path. Two bodies answer this today: an unknown `/v1/` path returns `error.type: "not_found"` with no `error.code` and a top-level `available_endpoints` list; other not-found answers carry `code: "endpoint_not_found"` and no list. | Check the path against the endpoint pages. | No |
| <span id="endpoint_retired">`endpoint_retired`</span> | 410 | none today | This endpoint was removed on purpose. **Not on the contract yet:** today's body has no `error.type`, `error.code: "endpoint_retired"`, and no `retryable` field or `x-should-retry` header. | Stop calling it. | No (no `retryable` field yet) |
| <span id="upload_timeout">`upload_timeout`</span> | 408 | `timeout_error` | **Not returned yet.** The upload stalled before it finished. | Retry, or send `audio_url` instead of a file. | Yes |

### Models

| Code | HTTP | Type | What happened | What to do | Retry |
| - | - | - | - | - | - |
| <span id="wrong_endpoint">`wrong_endpoint`</span> | 400 | `invalid_request_error` | The model exists, but this endpoint cannot serve it (for example an embedding model on a chat endpoint). | Call the endpoint `error.message` names. | No |
| <span id="model_not_found">`model_not_found`</span> | 404 | `not_found_error` | Kyma does not serve this model id, or it was retired. | Use an id from `did_you_mean` (a retired model's successor comes first) or `GET /v1/models`. | No |
| <span id="model_retired">`model_retired`</span> | 404 | `not_found_error` | **Not returned yet.** The model was retired; `did_you_mean[0]` is its successor. | Switch to the successor. | No |

### Billing

| Code | HTTP | Type | What happened | What to do | Retry |
| - | - | - | - | - | - |
| <span id="model_requires_paid">`model_requires_paid`</span> | 402 | `billing_error` | The account has never added credits and this model is not in the free tier. | Add credits once, or use a model with `free_tier: true`. | No |
| <span id="insufficient_credits">`insufficient_credits`</span> | 402 | `billing_error` | The balance does not cover this request; `need_usd` and `have_usd` say by how much. | Add at least `need_usd - have_usd`, or lower `max_tokens`. | No |
| <span id="tier_required">`tier_required`</span> | 402 | `billing_error` | **Not returned yet.** This capability is locked on your tier. | Add credits to reach the tier the message names. | No |
| <span id="payment_required">`payment_required`</span> | 402 | `billing_error` | **Not returned yet.** `/v1/topup` only: a payment challenge. | Pay as the `WWW-Authenticate` header describes, then resend. | Yes |
| <span id="settle_failed">`settle_failed`</span> | 500 | `api_error` | A top-up was paid but not credited yet. | Do not pay again; keep the payment reference and contact us. | No |

### Rate limits

| Code | HTTP | Type | What happened | What to do | Retry |
| - | - | - | - | - | - |
| <span id="request_exceeds_tier_limit">`request_exceeds_tier_limit`</span> | 413 | `request_too_large` | **Not returned yet.** The request's estimated tokens are over your tier's per-request cap. | Split the request, or add credits to raise your tier. | No |
| <span id="rate_limit_exceeded">`rate_limit_exceeded`</span> | 429 | `rate_limit_error` | Too many requests or tokens this minute for your tier, or one request larger than your tier's per-request token cap. | Wait `Retry-After` seconds, or send fewer requests. A single oversized request fails again until you split it. | Yes |
| <span id="concurrency_limit_exceeded">`concurrency_limit_exceeded`</span> | 429 | `rate_limit_error` | **Not returned yet.** Too many jobs in flight at once. | Wait `Retry-After`, or add credits for a higher tier. | Yes |
| <span id="media_rate_exceeded">`media_rate_exceeded`</span> | 429 | `rate_limit_error` | **Not returned yet.** Too many media jobs in the current window. | Wait `Retry-After`. | Yes |
| <span id="queue_full">`queue_full`</span> | 429 | none today | The media queue is full. **Not on the contract yet:** today's body has no `error.type`, `error.code: "queue_full"`, and no `retryable` field or `x-should-retry` header. | Wait `Retry-After` (30 seconds). | Yes (no `retryable` field yet) |
| <span id="too_many_sessions">`too_many_sessions`</span> | 429 | none today | Realtime session cap for your tier. **Not on the contract yet:** today's body has no `error.type`, `error.code: "too_many_sessions"`, and no `retryable` field or `x-should-retry` header. | End a session, or add credits. | No (no `retryable` field yet) |
| <span id="too_many_attempts">`too_many_attempts`</span> | 429 | `rate_limit_error` | Too many login, reset or new top-up account attempts from this address. | Wait and try again later. | Yes |

### Model availability

| Code | HTTP | Type | What happened | What to do | Retry |
| - | - | - | - | - | - |
| <span id="route_refused">`route_refused`</span> | 403 | `permission_error` | The model refused this content on every route. | Rephrase, or use a model from `error.suggested_models`. Retrying unchanged fails the same way. | No |
| <span id="upstream_rate_limited">`upstream_rate_limited`</span> | 429 | `rate_limit_error` | The model's capacity is busy. `lyria-3.5` returns this code; the other audio endpoints still send `provider_rate_limited`. | Wait `Retry-After`, or use a model from `suggested_models`. | Yes |
| <span id="provider_unavailable">`provider_unavailable`</span> | 503 | `upstream_error` | Every route for this model failed just now. | Retry after `Retry-After`, or use another model. | Yes |
| <span id="model_unavailable">`model_unavailable`</span> | 503 | `upstream_error` | The model cannot be served on this endpoint right now. | Retry later, or pick another model from `GET /v1/models`. | Yes |
| <span id="upstream_invalid_response">`upstream_invalid_response`</span> | 502 | `upstream_error` | The model answered with something unreadable or empty. | Retry. | Yes |
| <span id="upstream_timeout">`upstream_timeout`</span> | 504 | `timeout_error` | The model did not answer in time. Nothing was charged. | Retry. | Yes |
| <span id="stream_interrupted">`stream_interrupted`</span> | 200 (in stream) | `upstream_error` | A stream failed after it started (HTTP stays 200). | Retry the whole request. | Yes |

### Kyma

| Code | HTTP | Type | What happened | What to do | Retry |
| - | - | - | - | - | - |
| <span id="service_unavailable">`service_unavailable`</span> | 503 | `api_error` | Kyma could not read your account, or a capability is down. Nothing was charged. | Retry after a few seconds. | Yes |
| <span id="internal_error">`internal_error`</span> | 500 | `api_error` | Something failed on Kyma's side. | Retry once; if it repeats, send us the `request_id`. | Yes |

### Media jobs

| Code | HTTP | Type | What happened | What to do | Retry |
| - | - | - | - | - | - |
| <span id="job_not_found">`job_not_found`</span> | 404 | `not_found_error` | **Not returned yet.** The job id is unknown or belongs to another account. | Use the id from the create response. | No |

### Account and sign-in

| Code | HTTP | Type | What happened | What to do | Retry |
| - | - | - | - | - | - |
| <span id="invalid_login">`invalid_login`</span> | 401 | `authentication_error` | The email or password is wrong. | Check both, or reset the password. | No |
| <span id="invalid_google_token">`invalid_google_token`</span> | 401 | `authentication_error` | Google sign-in could not be verified. | Sign in with Google again. | No |
| <span id="captcha_failed">`captcha_failed`</span> | 400 | `invalid_request_error` | The signup captcha did not verify. | Complete the captcha again. | No |
| <span id="invalid_signature">`invalid_signature`</span> | 400 | `invalid_request_error` | A webhook arrived without a valid signature. | Only the payment and email services call this endpoint. | No |
| <span id="account_exists">`account_exists`</span> | 409 | `invalid_request_error` | An account with this email already exists. | Sign in instead. | No |
| <span id="signup_paused">`signup_paused`</span> | 429 | `rate_limit_error` | Signups are paused for a moment. | Try again later. | Yes |
| <span id="signup_failed">`signup_failed`</span> | 500 | `api_error` | The account could not be created. | Try again. | Yes |

### Older values

| Older value | Same meaning as |
| - | - |
| `audio_concurrent_limit` | [`concurrency_limit_exceeded`](#concurrency_limit_exceeded) |
| `audio_upload_timeout` | [`upload_timeout`](#upload_timeout) |
| `auth_error` | [`invalid_api_key`](#invalid_api_key) |
| `auth_required` | [`missing_api_key`](#missing_api_key) |
| `db_error` | [`internal_error`](#internal_error) |
| `fallback_unreachable` | [`upstream_invalid_response`](#upstream_invalid_response) |
| `forbidden` | [`permission_denied`](#permission_denied) |
| `image_rate_exceeded` | [`media_rate_exceeded`](#media_rate_exceeded) |
| `insufficient_balance` | [`insufficient_credits`](#insufficient_credits) |
| `invalid_credentials` | [`invalid_api_key`](#invalid_api_key) |
| `invalid_model` | [`wrong_endpoint`](#wrong_endpoint) |
| `invalid_request` | [`invalid_json`](#invalid_json), [`missing_parameter`](#missing_parameter), [`invalid_parameter`](#invalid_parameter), [`wrong_endpoint`](#wrong_endpoint), [`request_too_large`](#request_too_large) |
| `not_found` | [`endpoint_not_found`](#endpoint_not_found), [`job_not_found`](#job_not_found) |
| `payment_unavailable` | [`service_unavailable`](#service_unavailable) |
| `pricing_missing` | [`internal_error`](#internal_error) |
| `provider_empty_stream` | [`upstream_invalid_response`](#upstream_invalid_response) |
| `provider_invalid_response` | [`upstream_invalid_response`](#upstream_invalid_response) |
| `provider_key_missing` | [`model_unavailable`](#model_unavailable) |
| `provider_rate_limited` | [`upstream_rate_limited`](#upstream_rate_limited) |
| `provider_unreachable` | [`upstream_invalid_response`](#upstream_invalid_response) |
| `rate_limited` | [`too_many_attempts`](#too_many_attempts) |
| `server_error` | [`service_unavailable`](#service_unavailable), [`internal_error`](#internal_error) |
| `timeout` | [`upstream_timeout`](#upstream_timeout) |
| `unknown_sku` | [`invalid_parameter`](#invalid_parameter) |
| `unsupported_audio_type` | [`unsupported_media_type`](#unsupported_media_type) |
| `video_rate_exceeded` | [`media_rate_exceeded`](#media_rate_exceeded) |

## Billing errors (402)

A 402 is always about money or free-tier access, never about your payload. Every 402 has `error.type: "billing_error"` and names the rule in `error.code`:

* `model_requires_paid`: the account has never added credits and this model is outside the free tier (`free_tier` on [`GET /v1/models`](/api-reference/models-list) says which models a new key can call). `required` is 0, because no top-up amount turns a paid model into a free one: add credits once, then the model is available.
* `insufficient_credits`: the balance does not cover this request. `need_usd` is the most the request can cost as sent (prompt plus `max_tokens`) and `have_usd` is your balance. Add at least the difference, or lower `max_tokens`.

Two pointers come with every 402: `topup_url` is the billing page for a person, and `topup` is the endpoint an agent with a wallet can pay without a person. Only `POST /v1/topup` itself answers with a payment challenge.

## Rate limit errors (429)

`rate_limit_exceeded` means too many requests, or tokens, in the current minute for your tier. On `/v1/chat/completions` and `/v1/messages` the response carries `Retry-After` and the `x-ratelimit-limit-requests`, `x-ratelimit-remaining-requests` and `x-ratelimit-reset-requests` headers. Limits per tier are on [Limits and tiers](/api-reference/limits-tiers).

## Errors while streaming

Once a stream has started, the HTTP status is already 200. If the model fails mid-answer:

* On `/v1/chat/completions` the last chunk has `finish_reason: "error"` and an `error` object with `code: "stream_interrupted"`, followed by `data: [DONE]`.
* On `/v1/messages` the stream ends with `event: error` and `{"type":"error","error":{"type":"overloaded_error","code":"stream_interrupted",...}}`, which Anthropic SDKs raise as an error.

Retry the whole request. The text streamed before the failure is billed like any other output; the charge shows on the [Logs](https://kymaapi.com/logs) page.

## Responses that succeeded but need attention

* `x-kyma-max-tokens-clamped`: your balance covered fewer output tokens than `max_tokens` asked for, so the request ran with this lower limit.
* `x-kyma-served-model` differs from the model you asked for: another model answered.
* `finish_reason: "length"` with empty content on a reasoning model: reasoning used the whole `max_tokens`. Raise it.

## Troubleshooting

| Symptom | Likely cause | Fix |
| - | - | - |
| 401 right after creating a key | The key was copied with whitespace, or sent as `x-api-key` on an OpenAI-compatible endpoint | Send `Authorization: Bearer <key>` |
| 402 `model_requires_paid` on a new account | Signup credit covers free-tier models only | Add credits once, or pick a model with `free_tier: true` |
| 402 while the balance looks positive | `max_tokens` reserves more than the balance | Lower `max_tokens`, or add credits |
| 503 on one model while others work | That model's routes are down right now | Retry in a few seconds, or switch model |
| Stream ends with `finish_reason: "error"` | The model failed mid-answer | Retry the whole request |
| 200 with empty content on a reasoning model | Reasoning used the whole `max_tokens` | Raise `max_tokens` |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.