# Error codes (/reference/errors)



Your call was refused. Find the `code` below. Do what the row says.

<Callout type="warn" title="Branch on code and retryable, never on the message">
  `code` is a closed set of 25. `message` is one sentence you can show a person. Its wording can change.
</Callout>

## The error body [#the-error-body]

Same object on REST, MCP and the CLI.

```json title="Response: 404"
{
  "error": {
    "ok": false,
    "code": "not_found",
    "message": "No such generation. Check the id returned by submit_generation.",
    "retryable": false,
    "credits_charged": 0
  }
}
```

| Field             | Type    | What it is                                      |
| ----------------- | ------- | ----------------------------------------------- |
| `ok`              | `false` | Always `false` on a refusal.                    |
| `code`            | string  | One of the 25 codes below.                      |
| `message`         | string  | One safe sentence. Show it, don't parse it.     |
| `retryable`       | boolean | May you send the identical request again.       |
| `credits_charged` | number  | What this refused call cost. Almost always `0`. |

Extra fields appear only on some codes. They are left out when not set, never `null`.

| Field                 | On                                                                         | Use                                                                  |
| --------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `retry_after_seconds` | `rate_limited`, transient `request_blocked`, `submission_in_flight` (race) | Seconds to wait.                                                     |
| `in_flight`           | `submission_in_flight` (one per key)                                       | `generation_id` and `status` of the job already running. Wait on it. |
| `blocked_reason`      | `request_blocked`                                                          | The code that got this request blocked.                              |
| `capability_status`   | `capability_unavailable`, `required_plan`                                  | `coming_soon`, `retired`, `requires_plan` or `unknown`.              |
| `required_plan`       | `required_plan`                                                            | The plan that unlocks it, when known.                                |
| `shortfall`           | `insufficient_credits`                                                     | Credits missing.                                                     |
| `limit`               | `max_credits_exceeded`, `spend_limit_exceeded`                             | Which cap refused. `limit.bound_by` names it.                        |
| `connect_url`         | `connection_not_configured` (MCP only)                                     | Page where a person picks the workspace.                             |

### Where it arrives [#where-it-arrives]

| Surface | Shape                                                                                                                 |
| ------- | --------------------------------------------------------------------------------------------------------------------- |
| REST    | Under `error`, with the HTTP status from the table. `Retry-After` header on a 429 that has `retry_after_seconds`.     |
| MCP     | The whole object (no `error` wrapper) as JSON in one text block, with `isError: true`.                                |
| CLI     | Sentence and code on stderr. `--json` prints `{"error": {...}}` on stdout. Exit codes: [CLI commands](/cli/commands). |

## All codes [#all-codes]

| Code                        | HTTP | Retryable | What to do                                                                                                                                                               |
| --------------------------- | ---- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `not_authorized`            | 401  | no        | Key missing, malformed, unknown, revoked or expired (MCP: connection not authorized). Fix the header or create a new key.                                                |
| `no_workspace`              | 403  | no        | The account has no workspace. Create one in the app, then reconnect.                                                                                                     |
| `connection_not_configured` | 403  | no        | MCP only. The account has several workspaces. Send the person to `connect_url` to pick one.                                                                              |
| `workspace_unavailable`     | 410  | no        | The key's workspace is gone. This credential will never work again. Use a key from a live workspace.                                                                     |
| `required_plan`             | 403  | no        | The workspace plan does not include the agent API or this capability. Pick another capability, or a person upgrades.                                                     |
| `read_only_connection`      | 403  | no        | A read-only connection tried to start work. Reconnect with spend access.                                                                                                 |
| `insufficient_scope`        | 403  | no        | REST only. The key lacks the scope named in the message. Create a key with it.                                                                                           |
| `invalid_config`            | 400  | no        | Body is not JSON, not an object, fails the schema, or has an unknown key. The message lists up to 5 issues. Read `config_schema` from [capabilities](/api/capabilities). |
| `capability_unavailable`    | 404  | no        | Capability id unknown, retired or not live yet. Check `capability_status`, then list capabilities.                                                                       |
| `not_found`                 | 404  | no        | Id does not exist or belongs to another workspace. Never says which. Also returned for an unknown `/api/v1` path.                                                        |
| `input_not_ready`           | 409  | **yes**   | An upload is still in its safety scan. Wait a moment, send again.                                                                                                        |
| `insufficient_credits`      | 402  | no        | Not enough credits. A person tops up in the app.                                                                                                                         |
| `max_credits_exceeded`      | 402  | no        | Your `max_credits` is too low. Send at least `ceil(estimate * 1.1)`, or ask for something cheaper.                                                                       |
| `spend_limit_exceeded`      | 402  | no        | A workspace or key spend cap blocked it. Ask a workspace owner or admin.                                                                                                 |
| `estimate_changed`          | 409  | **yes**   | The price moved. Estimate again, raise `max_credits` if needed, resend.                                                                                                  |
| `not_priced`                | 409  | no        | This capability cannot be priced yet. Use another capability.                                                                                                            |
| `pricing_unavailable`       | 503  | **yes**   | Pricing failed for a moment. Retry shortly.                                                                                                                              |
| `moderation_blocked`        | 422  | no        | Refused by content policy. Change the wording or the file. Resending unchanged gets blocked. [Content policy](/policy/content).                                          |
| `moderation_unavailable`    | 503  | **yes**   | Content checks are down, so nothing can start. Reads still work. Retry later.                                                                                            |
| `rate_limited`              | 429  | **yes**   | Too many requests, uploads or renders at once. Wait `retry_after_seconds`, else back off. [Limits](/reference/limits).                                                   |
| `quota_exceeded`            | 429  | no        | The key used up its request allowance, or too many uploads are still processing. Waiting a few seconds won't fix it.                                                     |
| `submission_in_flight`      | 409  | **yes**   | Something is already running. Wait on it, don't resend. See below.                                                                                                       |
| `request_blocked`           | 429  | no        | This exact request is blocked after repeated failures. Send a different request. See below.                                                                              |
| `provider_unavailable`      | 502  | **yes**   | A provider or dependency failed. Back off, retry.                                                                                                                        |
| `internal_error`            | 500  | **yes**   | Our fault. Retry once or twice, then stop.                                                                                                                               |

8 codes are retryable. 17 are not.

<Callout type="error" title="The HTTP status does not tell you to retry">
  `rate_limited`, `quota_exceeded` and `request_blocked` are all 429. Only `rate_limited` is retryable. Read `retryable`.
</Callout>

## Codes with more than one cause [#codes-with-more-than-one-cause]

**`submission_in_flight`**

| Cause                                                         | Extra field               | Do                                                                                       |
| ------------------------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------- |
| This API key (or MCP person) already has a generation running | `in_flight`               | Wait on `in_flight.generation_id` with [`GET /generations/{id}/wait`](/api/generations). |
| Someone in the workspace sent the identical request first     | `retry_after_seconds: 30` | Wait for theirs, or change the request. Nothing was charged.                             |
| The workflow already has a run in flight                      | none                      | Wait for that run to finish.                                                             |

**`request_blocked`**

| Kind      | Armed after                                                                                       | Lasts        | `retry_after_seconds` |
| --------- | ------------------------------------------------------------------------------------------------- | ------------ | --------------------- |
| Transient | 3 failures in a row (`provider_unavailable`, `internal_error`, or a content refusal from a model) | about 1 hour | yes                   |
| Permanent | 2 content refusals from the rule list                                                             | 30 days      | no                    |

Both are `retryable: false`. Change the script or inputs. Adding junk fields does not make it a new request: unknown keys are refused first.

**`rate_limited`** from too many renders at once has **no** `retry_after_seconds`. Wait for a job to finish, then back off.

**`moderation_blocked`** on a file names the asset id (`ast_...`). That file can never be used. Upload a different one.

## Retry rules [#retry-rules]

* **`retryable: false`:** never send that request again. Not after a wait, not with a new key. Change it or stop.
* **`retryable: true`:** at most two more attempts. Honor `retry_after_seconds`, else back off. Then stop.
* **Lost the response?** Don't resubmit to find out. A second submit can be a second charge. Read the generation instead.

```ts
async function submit(body: unknown, attempt = 0): Promise<unknown> {
  const res = await fetch("https://app.riffads.com/api/v1/generations", {
    method: "POST",
    headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
    body: JSON.stringify(body),
  });
  const json = await res.json();
  if (res.ok) return json;

  const e = json.error;
  if (!e.retryable) throw new Refused(e.code, e.message);
  if (e.code === "submission_in_flight" && e.in_flight) {
    return waitFor(e.in_flight.generation_id);
  }
  if (attempt >= 2) throw new Refused(e.code, e.message);
  await sleep((e.retry_after_seconds ?? 2 ** attempt) * 1000);
  return submit(body, attempt + 1);
}
```

## A refusal is not a failed generation [#a-refusal-is-not-a-failed-generation]

* **Refusal:** the submit did not happen. Nothing started.
* **Failed generation:** the submit was accepted (201), then the job failed. You see it on a read: `status: "failed"` and `error: { code, message }`. That `code` can be `null`. [Results](/guides/results).

## The one 429 with a different shape [#the-one-429-with-a-different-shape]

The MCP host has a per-IP brake that answers before auth. It is OAuth shaped, with `Retry-After`. `/api/v1` is not behind it.

```json title="Response: 429"
{
  "error": "too_many_requests",
  "error_description": "Too many requests from this address. Please wait and try again."
}
```

## Codes that don't exist [#codes-that-dont-exist]

No `bad_request`, `conflict`, `forbidden`, `unauthorized`, `server_error`, `timeout`, `payload_too_large`, `unsupported_media_type`. Another workspace's id answers `not_found`, never `forbidden`.
