Reference

Error codes

Every refusal code, its HTTP status, whether to retry, and what to do.

Read as Markdown

Your 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.

Response: 404
{
  "error": {
    "ok": false,
    "code": "not_found",
    "message": "No such generation. Check the id returned by submit_generation.",
    "retryable": false,
    "credits_charged": 0
  }
}
FieldTypeWhat it is
okfalseAlways false on a refusal.
codestringOne of the 25 codes below.
messagestringOne safe sentence. Show it, don't parse it.
retryablebooleanMay you send the identical request again.
credits_chargednumberWhat this refused call cost. Almost always 0.

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

FieldOnUse
retry_after_secondsrate_limited, transient request_blocked, submission_in_flight (race)Seconds to wait.
in_flightsubmission_in_flight (one per key)generation_id and status of the job already running. Wait on it.
blocked_reasonrequest_blockedThe code that got this request blocked.
capability_statuscapability_unavailable, required_plancoming_soon, retired, requires_plan or unknown.
required_planrequired_planThe plan that unlocks it, when known.
shortfallinsufficient_creditsCredits missing.
limitmax_credits_exceeded, spend_limit_exceededWhich cap refused. limit.bound_by names it.
connect_urlconnection_not_configured (MCP only)Page where a person picks the workspace.

Where it arrives

SurfaceShape
RESTUnder error, with the HTTP status from the table. Retry-After header on a 429 that has retry_after_seconds.
MCPThe whole object (no error wrapper) as JSON in one text block, with isError: true.
CLISentence and code on stderr. --json prints {"error": {...}} on stdout. Exit codes: CLI commands.

All codes

CodeHTTPRetryableWhat to do
not_authorized401noKey missing, malformed, unknown, revoked or expired (MCP: connection not authorized). Fix the header or create a new key.
no_workspace403noThe account has no workspace. Create one in the app, then reconnect.
connection_not_configured403noMCP only. The account has several workspaces. Send the person to connect_url to pick one.
workspace_unavailable410noThe key's workspace is gone. This credential will never work again. Use a key from a live workspace.
required_plan403noThe workspace plan does not include the agent API or this capability. Pick another capability, or a person upgrades.
read_only_connection403noA read-only connection tried to start work. Reconnect with spend access.
insufficient_scope403noREST only. The key lacks the scope named in the message. Create a key with it.
invalid_config400noBody 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_unavailable404noCapability id unknown, retired or not live yet. Check capability_status, then list capabilities.
not_found404noId does not exist or belongs to another workspace. Never says which. Also returned for an unknown /api/v1 path.
input_not_ready409yesAn upload is still in its safety scan. Wait a moment, send again.
insufficient_credits402noNot enough credits. A person tops up in the app.
max_credits_exceeded402noYour max_credits is too low. Send at least ceil(estimate * 1.1), or ask for something cheaper.
spend_limit_exceeded402noA workspace or key spend cap blocked it. Ask a workspace owner or admin.
estimate_changed409yesThe price moved. Estimate again, raise max_credits if needed, resend.
not_priced409noThis capability cannot be priced yet. Use another capability.
pricing_unavailable503yesPricing failed for a moment. Retry shortly.
moderation_blocked422noRefused by content policy. Change the wording or the file. Resending unchanged gets blocked. Content policy.
moderation_unavailable503yesContent checks are down, so nothing can start. Reads still work. Retry later.
rate_limited429yesToo many requests, uploads or renders at once. Wait retry_after_seconds, else back off. Limits.
quota_exceeded429noThe key used up its request allowance, or too many uploads are still processing. Waiting a few seconds won't fix it.
submission_in_flight409yesSomething is already running. Wait on it, don't resend. See below.
request_blocked429noThis exact request is blocked after repeated failures. Send a different request. See below.
provider_unavailable502yesA provider or dependency failed. Back off, retry.
internal_error500yesOur 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

CauseExtra fieldDo
This API key (or MCP person) already has a generation runningin_flightWait on in_flight.generation_id with GET /generations/{id}/wait.
Someone in the workspace sent the identical request firstretry_after_seconds: 30Wait for theirs, or change the request. Nothing was charged.
The workflow already has a run in flightnoneWait for that run to finish.

request_blocked

KindArmed afterLastsretry_after_seconds
Transient3 failures in a row (provider_unavailable, internal_error, or a content refusal from a model)about 1 houryes
Permanent2 content refusals from the rule list30 daysno

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. 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.
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" and error: { code, message }. That code can be null. 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.

Response: 429
{
  "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.

On this page