Every error is a standard HTTP status with a JSON body, so your existing retry and backoff logic works unchanged.

## Error format

Errors come back as an `error` object inside `detail`. It has a stable `code` and a human-readable `message`; `type` repeats the `code`. Branch on `detail.error.code` in code, and use `message` for logs and for people.

```json
{
  "detail": {
    "error": {
      "message": "Invalid API key",
      "type": "invalid_api_key",
      "code": "invalid_api_key"
    }
  }
}
```

Some errors add fields to the same object. A spend-limit error, for example, also says which limit was hit.

## Status codes

| Status | Meaning | What to do |
|---|---|---|
| `400` | Bad request: malformed body, an unsupported parameter, or an invalid `provider` value. | Check the request against the upstream API schema. Unknown parameters are rejected, not silently forwarded. |
| `401` | Invalid or missing API key. This also covers a disabled or expired key, and a key whose owner has left the organization. | Check the auth header. In Console → API keys, look for a **Disabled**, **Expired**, or **Owner removed** status. Rotate the key if it leaked. |
| `402` | A spend limit was reached (`period_quota_exceeded`), or the wallet is out of funds (`insufficient_quota`). | Raise the budget in Console → Budgets, or add credits in Console → Credits. A spend-limit error includes the `period`, `limit`, `used`, `resets_at`, and `target_type`. |
| `429` | Rate limit reached (`rate_limit_exceeded`). | Back off and retry. See below. |
| `502` | Upstream network error reaching the model provider. | Transient. Retry with backoff. |
| `503` | No provider could serve the request. Either all routing retries were used up, or no upstream meets your data policy (`no_provider_matches_data_policy`). | Retry, choose another model, or relax the [data policy](/docs/data-policy). |
| `504` | The upstream model timed out. | Lower `max_tokens` or pick a faster model. |

### Why a `401` says so little

The response is the same for a disabled key, an expired key, a removed owner, and a key that never existed. This is deliberate: if it differed, anyone holding an old key could probe whether a person is still in an organization. To find out why a key fails, check the key list in the console.

### Spend-limit errors

A `402` with `period_quota_exceeded` means a daily, weekly, or monthly [budget](/docs/budgets) was hit. Budgets reset on a fixed UTC schedule.

```json
{
  "detail": {
    "error": {
      "message": "Daily spend limit reached",
      "type": "period_quota_exceeded",
      "code": "period_quota_exceeded",
      "period": "daily",
      "limit": 50000000,
      "used": 50000000,
      "resets_at": "2026-10-14T00:00:00.000Z",
      "target_type": "workspace"
    }
  }
}
```

| Field | Meaning |
|---|---|
| `period` | `daily`, `weekly`, or `monthly`. |
| `limit`, `used` | The limit and the spend so far, in quota units: 500,000 units = $1. The example above is a $100 daily limit. |
| `resets_at` | When the limit starts over, in UTC. |
| `target_type` | Whose budget was hit: `workspace` or `member`. Use it to tell the caller who can raise the limit. Handle the case where it is absent. |

An out-of-funds wallet is a `402` with `insufficient_quota`. Add credits to clear it. See [Credits](/docs/credits).

## Rate limits

Each account can send up to **600 requests per minute**, counted across all of its API keys. Image generation is limited to **60 per minute**. Requests over the limit get a `429` with `rate_limit_exceeded`. Wait and retry.
