# Errors

> Every error has a stable code. Branch on the code, not the message. This table is built from the API's own code list.

## The shape

Every error body is JSON:

```json
{
  "error": "Not enough credits.",
  "code": "INSUFFICIENT_CREDITS",
  "request_id": "req_...",
  "needed": 63,
  "balance": 12
}
```

- `code` is stable and machine-readable. Branch on it. Accept a code you do not know and treat it by its HTTP status.
- `request_id` is also the `X-Request-Id` header. Quote it when you ask diffusr for help. Send your own `X-Request-Id` (1 to 64 letters, digits, `_` or `-`) to choose it.
- Some codes add detail: `issues` (VALIDATION_FAILED, PREFLIGHT_FAILED), `needed` and `balance`, `budget`, `scope`, `limit`, `retry_after_seconds`, `field`, `index`.
- `429` and `503` answers carry a `Retry-After` header: seconds to wait.
- A refused request is never charged.
- MCP tool errors start with `[CODE]` and end with a `request_id:` line.

## Every code

| Code | HTTP | What it means | Retry? | What to do |
| --- | --- | --- | --- | --- |
| `UNAUTHORIZED` | 401 | Missing, malformed or unknown API key. | No | Send `Authorization: Bearer <key>`. Copy the key again from Developers in the app. |
| `KEY_REVOKED` | 401 | This key was revoked. Make a new one on the Developers page. | No | Make a new key on the Developers page and use that. |
| `KEY_EXPIRED` | 401 | This key passed its expiry date. Make a new one. | No | Make a new key on the Developers page. |
| `SCOPE_DENIED` | 403 | The key does not have the scope this call needs. | No | Use a key that has the scope the route names, or make a new key with it. |
| `MODEL_NOT_ALLOWED` | 403 | The key is limited to other models. | No | Pass a `model` the key may use (the default model counts), or use another key. |
| `TEST_MODE_UNSUPPORTED` | 403 | This call is not available with a test key. | No | Use a live key for this call. |
| `ACCOUNT_BLOCKED` | 403 | The account is blocked. Contact support. | No | Stop. Tell the user to contact diffusr. |
| `FORBIDDEN` | 403 | Not allowed (a refusal that has no more specific code). | No | Read `error` for the reason. Do not retry as is. |
| `VALIDATION_FAILED` | 422 | The input did not match the schema; `issues` lists each field and why. Nothing was made, nothing charged. | No | Read `issues`: each has a field path and a reason. Fix those fields and send again. Nothing was charged. |
| `INVALID_JSON` | 422 | The body was not a JSON object. | No | Send a JSON object with `Content-Type: application/json`. |
| `INVALID_REQUEST` | 400 or 422 | The request was understood but cannot be done as sent (a refusal that has no more specific code). | No | Read `error`, change the request, send again. |
| `PREFLIGHT_FAILED` | 422 | The job cannot run as described (for example a missing input); `issues` lists why. Nothing was charged. | No | Read `issues` for what is missing or unusable, fix it, send again. Nothing was charged. |
| `CALLBACK_URL_INVALID` | 422 | callback_url is not usable: it must be a public https address with no password. | No | Use a public `https://` address with no username or password. |
| `CALLBACK_NOT_RETRYABLE` | 409 | That callback event can't be retried now: it was delivered, is still being tried, or is out of manual retries. | No | Check the event in GET /callbacks. Only a FAILED event with manual retries left can be retried. |
| `REFERENCE_UNREACHABLE` | 422 | A reference file could not be fetched. | No | Make the link public and reachable without a login, or upload the file with POST /uploads. |
| `REFERENCE_TOO_LARGE` | 422 | A reference file is over the model's size or length limit. | No | Send a smaller or shorter file. Limits are on the Uploads and media page. |
| `REFERENCE_UNSUPPORTED` | 422 | A reference file is the wrong kind for this model. | No | Send the right kind of file: a picture for `image_urls`, audio for `audio_urls`, video for `video_url`. |
| `REFERENCE_FORBIDDEN` | 403 | A reference points at a file the caller may not use. | No | Use a link to your own upload or result, or any public link. Do not use `asset://` links. |
| `NOT_FOUND` | 404 | No such id, or it belongs to someone else (or to the other key mode: test vs live). | No | Check the id. A test key cannot see live results, and a live key cannot see test results. |
| `IDEMPOTENCY_CONFLICT` | 409 | This Idempotency-Key was already used for a different request. | No | Use a new Idempotency-Key for a different request. Reuse the old key only to repeat the exact same request. |
| `CONFLICT` | 409 | The request conflicts with the current state (a refusal that has no more specific code). | No | Read `error`, fetch the current state, then decide. |
| `INSUFFICIENT_CREDITS` | 402 | Not enough free credits; `needed` and `balance` say by how much. | No | Tell the user. `needed` and `balance` say how short they are. They can top up in the app. |
| `allowance_exceeded` | 402 | A team member's limit or the team pot is used up (legacy lowercase code, kept for existing clients). | No | Tell the user. A team limit is used up. |
| `BUDGET_EXCEEDED` | 402 | The key's or account's daily or monthly API budget would be passed; `budget` has the window and when it frees up. | No | Tell the user. `budget.resets_at` says when the window frees up. Do not retry before then. |
| `RATE_LIMITED` | 429 | Too many requests of this kind this minute. | After `Retry-After` | Wait `Retry-After` seconds, then send the same request again (same Idempotency-Key). |
| `CONCURRENCY_LIMIT` | 429 | Too many jobs queued or running for this key or account. | After `Retry-After` | Wait `Retry-After` seconds, or wait for a running job to finish. The request was refused, not queued. |
| `PROVIDER_BUSY` | 503 | Our capacity at the model's supplier is full. Not the caller's fault; try again. | After `Retry-After` | Wait `Retry-After` seconds, then retry. Not your fault; nothing was charged. |
| `AUTH_RATE_LIMITED` | 429 | Too many failed sign-in attempts from this address. | After `Retry-After` | Stop sending bad keys. Wait `Retry-After` seconds. |
| `KEY_LIMIT` | 429 | The key hit its own limit (for example how many keys an account may hold). | After `Retry-After` | Revoke keys you do not use, or wait `Retry-After` seconds before making more. |
| `TEST_LIMIT` | 429 | A test key hit its own cap (per minute or per day). Wait, or use a live key. | After `Retry-After` | Wait `Retry-After` seconds, or use a live key. |
| `UPLOAD_QUOTA` | 429 | Too many uploads in the last 24 hours. | After `Retry-After` | Wait `Retry-After` seconds. Reuse a `file_url` you already have instead of uploading again. |
| `STORAGE_QUOTA` | 413 | The account's stored upload bytes are at the cap; uploads clear on their own. | After `Retry-After` | Wait: uploads clear on their own after their retention time. |
| `TOO_LARGE` | 413 | The request or file is too large (a refusal that has no more specific code). | No | Send a smaller request or file. |
| `WALLET_BUSY` | 503 | The wallet was locked by another request. Retry at once. | Yes, at once | Retry at once with the same Idempotency-Key (`Retry-After` is 2 seconds). |
| `API_PAUSED` | 503 | New API starts are paused for everyone. Runs already started finish. | After `Retry-After` | New starts are paused. Wait `Retry-After` seconds. Dry runs still answer. |
| `MODEL_PAUSED` | 503 | This model is paused for new API starts. | After `Retry-After` | Use another model, or wait `Retry-After` seconds. |
| `PROVIDER_PAUSED` | 503 | This model's supplier is paused for new API starts. | After `Retry-After` | Use another model, or wait `Retry-After` seconds. |
| `UNAVAILABLE` | 503 | A service we depend on is not available right now (a refusal that has no more specific code). Retry later. | After `Retry-After` | Wait `Retry-After` seconds if present (else a minute), then retry with the same Idempotency-Key. |
| `INTERNAL_ERROR` | 500 | Something went wrong on our side. Quote the request_id to support. | After `Retry-After` | Retry once with the same Idempotency-Key. If it keeps failing, quote `request_id` to diffusr. |

## Retrying safely

Retry only the codes marked "After `Retry-After`" or "Yes, at once", and always with the **same** Idempotency-Key. If the first try had already been accepted you get that job back (`idempotent_replay: true`) and are not charged twice.

Source: https://diffusr.ai/docs/errors
