Error codes
Every refusal code, its HTTP status, whether to retry, and what to do.
Read as MarkdownYour call was refused. Find the code below. Do what the row says.
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.
The error body
Same object on REST, MCP and the CLI.
{
"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
| 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. |
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. |
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. |
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. |
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.
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.
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. |
| 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
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. Honorretry_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.
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
- 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"anderror: { code, message }. Thatcodecan benull. Results.
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.
{
"error": "too_many_requests",
"error_description": "Too many requests from this address. Please wait and try again."
}Codes that don't 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.