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

# Error handling

> Every error code, what it means, and what to do about it.

Errors use the OpenAI-compatible shape — an `error` object with `message`, `type`,
`code`, and `param`. The same `message` and `code` are mirrored at the top
level, alongside a `request_id` for support. Some errors add extra top-level
fields (`retry_after` on a `429`; `scope`, `limit`, and `spent` on a spend-limit
`402`; `maximum_hold_usd` on `max_cost_exceeded`):

```json theme={null}
{
  "error": {
    "message": "No API credits. Add credits at https://www.ninjachat.ai/developers/billing — docs: https://docs.ninjachat.ai",
    "type": "insufficient_quota",
    "code": "insufficient_credits",
    "param": null
  },
  "message": "No API credits. Add credits at https://www.ninjachat.ai/developers/billing — docs: https://docs.ninjachat.ai",
  "code": "insufficient_credits",
  "request_id": "req_abc123",
  "balance": "$0.00"
}
```

That's the zero-balance case, checked before the body is read. If you have a
balance but it can't cover this request's pre-authorization hold, chat returns
the same code with a dynamic message: `"Insufficient credits for the maximum
token hold of $0.012345."` The hold is a reserve sized from your input plus the
output ceiling; a completed request settles to actual usage.

## Error reference

### 400 — fix the request

| Code                            | What's wrong                                                                                                                                                            | What to do                                                                                              |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `invalid_json`                  | Body isn't valid JSON                                                                                                                                                   | Fix your JSON syntax                                                                                    |
| `validation_error`              | A field is missing, the wrong type, or out of range; `param` names it                                                                                                   | Check the message against [Parameters](/chat#parameters)                                                |
| `unsupported_parameter`         | An unknown field (request schemas are strict), or a real use of `logprobs` / `logit_bias` on `/chat/completions`                                                        | Remove the field — see [OpenAI compatibility](/openai-compatibility#whats-rejected)                     |
| `conflicting_parameters`        | `max_tokens` and `max_completion_tokens` both sent with different values                                                                                                | Send one of them                                                                                        |
| `unsupported_value`             | `n` other than `1`                                                                                                                                                      | Send separate requests                                                                                  |
| `invalid_parameter`             | `metadata` isn't an object with at most 32 keys                                                                                                                         | Fix the value                                                                                           |
| `invalid_model`                 | Retired id or syntax — `ensemble*`, `auto-*`, `a>b` chains, `model:suffix` — or `ninja/auto` after the first entry of `models`                                          | Use `ninja/auto`, `models`, and `routing`                                                               |
| `unknown_model`                 | `model` isn't in the catalog                                                                                                                                            | Copy a canonical ID from [`GET /models`](/models)                                                       |
| `model_not_vision_capable`      | Image input sent to a model without vision                                                                                                                              | Use a vision-capable model                                                                              |
| `model_not_tool_capable`        | `tools` sent to a model without tool calling                                                                                                                            | Use a tool-capable model                                                                                |
| `model_not_json_capable`        | `response_format` / `text.format` JSON sent to a model without structured output                                                                                        | Use a model with the `json_mode` capability                                                             |
| `max_cost_exceeded`             | The maximum token hold is above `routing.max_cost_usd` (`maximum_hold_usd` is returned)                                                                                 | Raise the cap, lower the output ceiling, or pick cheaper candidates — [Spend controls](/budget-routing) |
| `unpriced_model`                | A model in a [`/compare`](/compare) or [`/batch`](/batch) request has no metered price; returned before anything runs (`param` names it, and batch adds `failed_index`) | Pick a model with `pricing` on [`GET /models`](/models)                                                 |
| `store_unsupported`             | `store: true` on `/responses`                                                                                                                                           | Responses are stateless; omit it or send `false`                                                        |
| `previous_response_unsupported` | `previous_response_id` on `/responses`                                                                                                                                  | Include the complete input instead                                                                      |
| `unsupported_tool`              | A non-`function` tool type on `/responses`                                                                                                                              | Only function tools are supported                                                                       |
| `invalid_image`                 | An `image_url` couldn't be fetched, isn't an image, or exceeds limits                                                                                                   | Send a public HTTPS URL or a base64 data URL                                                            |
| `invalid_period`                | `period` isn't `1d`, `7d`, or `30d` (usage endpoint)                                                                                                                    | Pass a supported period                                                                                 |

### 401 / 403 — fix the credential

| Status | Code                 | What's wrong                            | What to do                                                                        |
| ------ | -------------------- | --------------------------------------- | --------------------------------------------------------------------------------- |
| 401    | `missing_api_key`    | No auth header                          | Add `Authorization: Bearer nj_sk_...`                                             |
| 401    | `invalid_api_key`    | Bad or revoked key                      | Create a new one in [Developers → Keys](https://www.ninjachat.ai/developers/keys) |
| 401    | `expired_api_key`    | Key passed its expiry                   | Create a new key                                                                  |
| 403    | `read_only_key`      | Key is read-only                        | Use a full-access key for billable requests                                       |
| 403    | `insufficient_scope` | The credential can't call this endpoint | Use a regular `nj_sk_` API key                                                    |

### 402 — add credits or raise a limit

| Code                   | What's wrong                                                                                          | What to do                                                                                                      |
| ---------------------- | ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `insufficient_credits` | Zero balance, or the balance can't cover the hold for this request                                    | [Add credits](https://www.ninjachat.ai/developers/billing)                                                      |
| `spend_limit_exceeded` | A monthly account, key, or project spend limit is reached; `scope`, `limit`, and `spent` are returned | Raise it at [Developers → Billing](https://www.ninjachat.ai/developers/billing) or wait for the period to reset |

### 404, 409, 410, 413, 429

| Status | Code                        | What's wrong                                                                                                                                                       | What to do                                               |
| ------ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- |
| 404    | `not_found`                 | Video job, request trace, webhook, or session doesn't exist or isn't yours                                                                                         | Check the ID and API-key ownership                       |
| 404    | `model_not_found`           | `GET /models/{id}` for an id that isn't in the catalog                                                                                                             | See `GET /models`                                        |
| 409    | `idempotency_conflict`      | The `Idempotency-Key` was already used with a different request body                                                                                               | Use a new key for new content                            |
| 409    | `request_in_flight`         | A request with this `Idempotency-Key` is still running (or failed before completing)                                                                               | Retry after `Retry-After` (5s)                           |
| 409    | `stream_replay_unsupported` | The key already completed as a stream; streams can't be replayed                                                                                                   | Use a new key                                            |
| 409    | `stream_not_replayable`     | The same case on [`/compare`](/compare) and [`/batch`](/batch): the `Idempotency-Key` already completed as a streamed comparison or batch, which can't be replayed | Use a new key                                            |
| 410    | `endpoint_retired`          | A retired legacy route; the body carries `successor` and `docs_url`                                                                                                | Move to the successor                                    |
| 413    | `payload_too_large`         | Body exceeds the route's byte limit                                                                                                                                | Shrink the request                                       |
| 429    | `rate_limit_exceeded`       | Too many requests — `limit` and `scope` say which layer                                                                                                            | Wait `Retry-After` seconds — [Rate limits](/rate-limits) |

### 5xx — retry

| Status | Code                      | What's wrong                                                                                                                                                                      | What to do                                                |
| ------ | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| 500    | `internal_error`          | Server error                                                                                                                                                                      | Retry — **not charged**                                   |
| 500    | `model_not_priced`        | A requested model has no metered price in the runtime catalog                                                                                                                     | Pick another model and report it                          |
| 500    | `batch_request_failed`    | A [`/batch`](/batch) sent with `fail_on_any_error: true` had one job fail; `failed_index` names it and the entire hold is refunded                                                | Fix or drop that request and retry — **not charged**      |
| 502    | `all_providers_failed`    | Every eligible model and rail failed before producing output (on a `stream: true` request this arrives as an in-band error frame on the already-open `200` stream, then `[DONE]`) | Retry or widen `models` — **not charged**                 |
| 502    | `generation_failed`       | Image or video generation failed upstream                                                                                                                                         | Retry — **not charged**; video jobs are refunded          |
| 502    | `search_failed`           | Search providers temporarily unavailable                                                                                                                                          | Retry — **not charged**                                   |
| 502    | `status_check_failed`     | Polling the video provider for job status failed                                                                                                                                  | Retry                                                     |
| 503    | `idempotency_unavailable` | The idempotency store is unreachable; no work was started                                                                                                                         | Retry with the same `Idempotency-Key` after `Retry-After` |
| 503    | `no_eligible_model`       | No requested model is currently servable under this routing policy                                                                                                                | Loosen `routing` or add candidates                        |
| 503    | `service_unavailable`     | Video provider not configured                                                                                                                                                     | Try again later                                           |

Streams can also fail after the HTTP status has been sent. In that case the last data frame is an error object — `partial_stream` (some output was delivered; only delivered tokens are billed) or `stream_error` (nothing was delivered; not charged). See [Streaming](/streaming).

## Error handling code

The official SDKs retry `429` and `500`/`502`/`503`/`504` when the request is safe to replay, honor `Retry-After`, and generate an `Idempotency-Key` for billed requests so a retry can only replay. A `409 request_in_flight` is retried only when the request carried an idempotency key; `408` is never retried. The clients expose typed errors:

<CodeGroup>
  ```typescript TypeScript SDK theme={null}
  import { NinjaChatError } from "@ninjachat/sdk";

  try {
    await client.responses.create({
      model: "gpt-5.6-luna",
      input: "Summarize this incident.",
    });
  } catch (error) {
    if (error instanceof NinjaChatError) {
      console.error(error.status, error.code, error.requestId);
    }
    throw error;
  }
  ```

  ```python Python SDK theme={null}
  from ninjachat import NinjaChatError

  try:
      client.responses.create(
          model="gpt-5.6-luna",
          input="Summarize this incident.",
      )
  except NinjaChatError as error:
      print(error.status, error.code, error.request_id)
      raise
  ```
</CodeGroup>

## Common mistakes

| Symptom                        | Cause                                                              | Fix                                                                                                         |
| ------------------------------ | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| `unknown_model`                | Typo in model ID                                                   | Check [available models](/models) for exact IDs                                                             |
| `unsupported_parameter`        | A field from another API (or `session_id` on a canonical endpoint) | Compare against the [parameter table](/chat#parameters)                                                     |
| `validation_error` on messages | Missing `role` or `content` field                                  | Each message needs `{"role": "user", "content": "..."}`                                                     |
| `insufficient_credits`         | Balance is \$0.00, or too low for this request's hold              | [Add credits](https://www.ninjachat.ai/developers/billing)                                                  |
| `invalid_api_key`              | Key revoked or copied incorrectly                                  | Keys start with `nj_sk_`. Create a new one in [Developers → Keys](https://www.ninjachat.ai/developers/keys) |
| Empty streaming response       | Not reading SSE correctly, or treating `:` keepalive lines as data | See [streaming guide](/streaming)                                                                           |

## Monitor before a `402`

Query `client.balance()` on a schedule and register a signed `balance.low` webhook for the prepaid `$5` and `$1` thresholds. Project spend limits also emit `budget.alert` at 50%, 80%, and 100%.

See [Observability](/observability) for the complete operating loop and signature verification.

## Quick debug

```bash theme={null}
# Test if your key works (should print 200)
curl -s -o /dev/null -w "%{http_code}" \
  -X POST https://www.ninjachat.ai/api/v1/responses \
  -H "Authorization: Bearer nj_sk_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-5.6-luna","input":"hi","max_output_tokens":8}'
```

| Output | Meaning                          |
| ------ | -------------------------------- |
| `200`  | Working                          |
| `401`  | Bad, missing, or expired API key |
| `403`  | Read-only key                    |
| `402`  | No credits                       |
