> ## 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 also mirrored at the top
level, alongside a `request_id` for support:

```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"
}
```

That's the zero-balance case. If you have some balance left but not enough to
cover the request, the message is dynamic instead: `"Insufficient credits. This
request costs $0.012. Balance: $0.01"`.

## Error reference

| Status | Code                       | What's wrong                                              | What to do                                                 |
| ------ | -------------------------- | --------------------------------------------------------- | ---------------------------------------------------------- |
| 400    | `invalid_json`             | Body isn't valid JSON                                     | Fix your JSON syntax                                       |
| 400    | `validation_error`         | Bad parameter(s)                                          | Check the `details` array (see below)                      |
| 400    | `unknown_model`            | `model` isn't a recognized ID                             | Check [available models](/models)                          |
| 400    | `no_model_in_budget`       | No model fits the requested `budget_cents`                | Raise the budget or omit it                                |
| 400    | `invalid_fallback_chain`   | Malformed `model` fallback-chain string                   | Check [fallback chains](/fallback-chains) syntax           |
| 400    | `model_not_vision_capable` | Model doesn't accept image input                          | Use a vision-capable model                                 |
| 400    | `model_not_tool_capable`   | Model doesn't support `tools`                             | Use a tool-capable model                                   |
| 400    | `invalid_period`           | `period` isn't `1d`, `7d`, or `30d` (usage endpoint)      | Pass a supported period                                    |
| 401    | `missing_api_key`          | No auth header                                            | Add `Authorization: Bearer nj_sk_...`                      |
| 401    | `invalid_api_key`          | Bad or revoked key                                        | Check key starts with `nj_sk_`                             |
| 402    | `insufficient_credits`     | Zero balance, or this request costs more than what's left | [Add credits](https://www.ninjachat.ai/developers/billing) |
| 404    | `not_found`                | Session or video job doesn't exist, or isn't yours        | Check the ID                                               |
| 413    | `image_too_large`          | Image content exceeds the size limit                      | Shrink or compress the image                               |
| 429    | `rate_limit_exceeded`      | Too many requests                                         | Wait `Retry-After` seconds                                 |
| 500    | `internal_error`           | Server error                                              | Retry                                                      |
| 500    | `batch_request_failed`     | One request in a `/batch` call failed                     | Check `failed_index` and retry that request                |
| 502    | `generation_failed`        | Image/video generation failed upstream                    | Retry — **not charged**                                    |
| 502    | `empty_completion`         | Chat model returned an empty response                     | Retry — **not charged**                                    |
| 502    | `empty_generation`         | Model returned no images                                  | Retry — **not charged**                                    |
| 502    | `search_failed`            | Search providers temporarily unavailable                  | Retry — **not charged**                                    |
| 502    | `status_check_failed`      | Polling the video provider for job status failed          | Retry                                                      |
| 503    | `service_unavailable`      | Video provider not configured                             | Try again later                                            |

## Validation errors

400 errors include a `details` array telling you exactly which fields are wrong:

```json theme={null}
{
  "error": {
    "message": "Invalid parameters.",
    "type": "invalid_request_error",
    "code": "validation_error",
    "param": "messages"
  },
  "message": "Invalid parameters.",
  "code": "validation_error",
  "details": [
    { "field": "messages", "message": "Required" },
    { "field": "model", "message": "Invalid model ID" }
  ]
}
```

## Error handling code

<CodeGroup>
  ```python Python theme={null}
  import os, time, requests

  HEADERS = {"Authorization": f"Bearer {os.environ['NINJACHAT_API_KEY']}"}

  def chat(model, message, retries=3):
      for attempt in range(retries):
          r = requests.post("https://www.ninjachat.ai/api/v1/chat",
              headers=HEADERS,
              json={"model": model, "messages": [{"role": "user", "content": message}]}
          )

          if r.status_code == 200:
              return r.json()["choices"][0]["message"]["content"]

          if r.status_code == 429:  # Rate limited — retry
              time.sleep(int(r.headers.get("Retry-After", 2 ** attempt)))
              continue

          if r.status_code >= 500:  # Server error — retry
              time.sleep(2 ** attempt)
              continue

          # Client errors — don't retry
          err = r.json()
          raise Exception(f"{r.status_code} {err['code']}: {err['message']}")

      raise Exception("Max retries exceeded")
  ```

  ```javascript Node.js theme={null}
  async function chat(model, message, retries = 3) {
    for (let i = 0; i < retries; i++) {
      const r = await fetch("https://www.ninjachat.ai/api/v1/chat", {
        method: "POST",
        headers: {
          "Authorization": `Bearer ${process.env.NINJACHAT_API_KEY}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ model, messages: [{ role: "user", content: message }] }),
      });

      if (r.ok) return (await r.json()).choices[0].message.content;

      if (r.status === 429) { // Rate limited — retry
        await new Promise(r => setTimeout(r, (r.headers.get("Retry-After") || 2 ** i) * 1000));
        continue;
      }
      if (r.status >= 500) { // Server error — retry
        await new Promise(r => setTimeout(r, 2 ** i * 1000));
        continue;
      }

      const err = await r.json();
      throw new Error(`${r.status} ${err.code}: ${err.message}`);
    }
    throw new Error("Max retries exceeded");
  }
  ```
</CodeGroup>

## Common mistakes

| Symptom                        | Cause                                             | Fix                                                                                                                                       |
| ------------------------------ | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `validation_error` on model    | Typo in model ID (e.g. `gpt5` instead of `gpt-5`) | Check [available models](/models) for exact IDs                                                                                           |
| `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    | [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 → Connections → Apps](https://www.ninjachat.ai/developers/connections?tab=apps) |
| Empty streaming response       | Not reading SSE stream correctly                  | See [streaming guide](/streaming)                                                                                                         |

## Proactive warnings

You don't have to wait for a `402` error. Responses include a `balance_warning` when your balance drops below \$5.00 — on every generated response, with one exception: a cache hit on `/chat` (`"cached": true`) skips it, since no new spend happened:

```json theme={null}
{
  "balance_warning": {
    "warning": "low_balance",
    "threshold": "Balance is $3.50. Consider adding credits soon."
  }
}
```

Check for `balance_warning` in responses and alert yourself before running out. See [Pricing](/pricing#low-balance-warnings) for details.

## 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/chat \
  -H "Authorization: Bearer nj_sk_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-5-mini","messages":[{"role":"user","content":"hi"}]}'
```

| Output | Meaning     |
| ------ | ----------- |
| `200`  | Working     |
| `401`  | Bad API key |
| `402`  | No credits  |
