---
title: Errors and spend
description: Every error the Formats API returns, by stage. The envelope, what to do per code, what a failed run carries, webhook delivery outcomes, credits and spend caps, and rate limits.
---

Every error on the API has the same envelope, and every code is a lowercase token you can
branch on. This page lists all of them for the Formats surface in one place, grouped by when
they happen: before a run exists, while you read it, when the run itself fails, and when a
webhook could not be delivered. Spend and rate limits are at the end.

## The error envelope

```json
{
  "error": {
    "code": "workspace_key_required",
    "message": "This Format belongs to a team workspace. Use an API key created in that workspace.",
    "request_id": "req_0123456789abcdef0123456789abcdef",
    "category": "auth",
    "stage": "auth",
    "retryable": false,
    "retry_after_seconds": null,
    "public_reason": "workspace_key_required",
    "next_action": "authenticate",
    "details": { "workspace_id": "org_…" }
  }
}
```

| Field | Use it for |
|---|---|
| `code` | The stable token to `switch` on. `^[a-z0-9_]+$`, never a sentence. |
| `message` | Written for a human, may change. Log it; never match on it. |
| `request_id` | Also sent as the `x-sume-request-id` header. Quote it to support. |
| `retryable`, `retry_after_seconds` | Whether resending the same request can succeed, and how long to wait first. |
| `next_action` | `authenticate`, `fix_input`, `add_funds`, `retry_later`, `poll_status`, `contact_support` or `none`. |
| `category`, `stage`, `public_reason` | Coarser labels for dashboards and alerts. |
| `details` | Code-specific: `required_scope`, `workspace_id`, `violations[]`, `index`, `status`, `scope`, … named under each code below. |

Branch on the HTTP status first, then on `code`. A `4xx` at create means nothing ran and
nothing was charged, so fix the call rather than retrying it. Retrying a
`403 insufficient_scope` in a loop is the most common and most expensive mistake.

## Errors at create

`POST /v1/formats/{handle}/{slug}/runs` and `POST …/bulk-runs`. Nothing runs, nothing is
charged, and a failed create releases its `Idempotency-Key`.

| Status | `error.code` | Meaning | What to do |
|---|---|---|---|
| 400 | `invalid_request` | The body names none of `instruction` / `input` / `previous_run_id` / `attachments`; both `output_schema` and `response_format`; `input` is not an object, has more than 64 top-level keys or is over 2 MiB; `generation_spend_cap_usd` is `0` or over 500; `model` is not in the catalog; a bulk body has bad `concurrency` or `items`; a webhook URL is not public HTTPS; conflicting `communication` aliases. | Read `message` and `details`. |
| 400 | `unknown_parameter` | A top-level field the API does not know, including `thread_id`. `details.errors[].suggestion` names the likely intended field. | Rename or remove it. |
| 400 | `output_schema_invalid` | The schema is outside the supported subset. `details.violations[]` lists every `{ path, rule, message }`. | Fix each violation. See [Supported schemas](/formats/structured-output#supported-schemas). |
| 400 | `invalid_attachment` | Bad `attachments[]` item, or the media budget `attachments[]` shares with `input` URLs is exceeded. | See [Attachment errors](/formats#attachment-errors). |
| 400 | `attachment_not_found` | `asset_id` is unknown in this workspace. | Upload it, or use a URL. |
| 400 | `previous_run_format_mismatch` | `previous_run_id` was created on a different Format. | Continue it on that Format. |
| 400 | `previous_run_not_resumable` | That run left nothing to continue. `details` reports `previous_run_status`, `has_thread`, `artifact_count`. | Start a fresh run. |
| 401 | `unauthorized` | No key, malformed key, revoked key, a key for the other host, or two credentials at once. | Fix the header. |
| 402 | `insufficient_credits` | The workspace wallet cannot fund the run. `next_action` is `add_funds`. | Top up. Retrying without doing so returns the same answer. |
| 402 | `organization_wallet_not_provisioned` | An organization workspace with no funded wallet. | An admin has to fund it. |
| 403 | `insufficient_scope` | The key lacks `formats:write` (`details.required_scope`), or it is a service-account key (`details.reason: service_account_format_runs_unsupported`). | Mint a new key with the scopes; scopes cannot be added to an existing one. |
| 403 | `workspace_key_required` | Team Format, personal key. `details.workspace_id` names the workspace. | Create a key in that workspace. |
| 404 | `format_not_found` | Unknown handle or slug, an archived Format, or one outside your key's workspace. | Check the address and the key. |
| 404 | `previous_run_not_found` | `previous_run_id` is unknown or not yours. | Check the id. |
| 409 | `format_inactive` | The Format is inactive. | Activate it in the dashboard. |
| 409 | `format_api_trigger_disabled` | The API trigger is off for this Format. | Turn it on in the dashboard. |
| 409 | `format_run_in_progress` | `on_active_run: "reject"` and a run is already in flight. | Wait, or drop `reject`. |
| 409 | `format_not_forkable` | You addressed a built-in capability rather than a Format. | Call a Format by Sume, or your own. |
| 409 | `previous_run_not_terminal` | The run you want to continue is still running. | Poll it, then call again. |
| 409 | `idempotency_conflict` | That `Idempotency-Key` was already used with a different body. On bulk, `details.queue_id` names the original queue. | Fix your key derivation; do not retry as is. |
| 409 | `idempotency_key_in_use` | Another request with the same key is in flight. `retryable: true`. | Wait about a second and resend. |
| 413 | `payload_too_large` | The request body is over 4 MiB (`details.limit_bytes`). | Shrink `input`; send media by URL. |
| 415 | `unsupported_media_type` | The body was not sent as `application/json` (`details.received_content_type`). | Set `Content-Type: application/json`. |
| 413 | `attachment_too_large` | An image over 30 MB, or a set over 500 MB. | Resize. |
| 429 | `rate_limited` | The write budget for this key is spent. `error.details.scope` is `write`. | Wait `retry-after`. See [Rate limits](#rate-limits). |
| 502 | `attachment_fetch_failed` | Sume could not fetch an attachment (`details.index`). Despite the `5xx`, it is your input: `next_action` is `fix_input`. | Make the URL publicly reachable. |
| 503 | `studio_agent_upstream_unavailable` | A Sume-side outage. | Retry later with the same `Idempotency-Key`. |
| 4xx/5xx | `format_run_failed_to_start` | The run could not start for a reason with no more specific code. | Read `message`; retry once, then contact support with `request_id`. |

A `202` never turns into one of these later: once you hold a receipt, failures arrive on it as
`status: "failed"`.

## Errors while reading

`GET /v1/format-runs/{run_id}`, `/status`, `/result`, `/events`, `POST …/cancel`,
`POST …/webhook/redeliver`, `GET /v1/format-run-queues/{queue_id}`, and the Format reads.

| Status | `error.code` | Meaning | What to do |
|---|---|---|---|
| 401 | `unauthorized` | As above. | Fix the header. |
| 403 | `insufficient_scope` | Reads need `formats:read`; cancel and redeliver need `formats:write`. | Mint a new key. |
| 404 | `format_run_not_found` | Unknown run id, or a run belonging to another owner. | Check the id and the key. A run you cannot see reads the same as one that does not exist. |
| 404 | `format_run_queue_not_found` | Unknown queue, or another owner's. | Same. |
| 404 | `format_not_found` | As above. | Same. |
| 409 | `run_not_completed` | `GET …/result` before the run is terminal. `details.status` is the current status; `retry_after_seconds` suggests the next poll. | Poll `status_url`, then read `result_url`. |
| 409 | `webhook_not_configured` | Redeliver on a run created without a `webhook_url`. | Nothing to redeliver. |
| 409 | `run_not_terminal` | Redeliver while the run is still running. | Wait for the terminal receipt. |
| 429 | `rate_limited` | The read budget is spent (`details.scope: read`). | Wait `retry-after`. The run keeps executing. |
| 503 | `studio_agent_upstream_unavailable` | A Sume-side outage. | Retry. The run keeps executing. |

A `429` or `503` inside a poll loop is transient. Abandoning the loop does not stop the run or
its spend, so back off and poll again.

## When the run itself fails

A run that could not finish comes back with `status: "failed"`, `error: { code, message }`,
and usually `output_error` with more detail. `artifacts[]` still lists everything the run
generated, and `output` still carries whatever partial result satisfied your schema. What a
failure never gets is a pointer: `primary_output_url` is `null`, so
`if (run.primary_output_url)` stays a safe test for "the deliverable exists".

| `error.code` | Meaning | What to do |
|---|---|---|
| `unattended_blocked` | The run hit a gate it could not pass without a person: no avatar matched the brief, or a missing input it would have asked about in chat. `message` is written to be shown. | Fix the input or the brief; retry with a new `Idempotency-Key`. |
| `output_schema_unsatisfied` | The run finished, but its result did not match your `output_schema`, or referenced media it did not produce. `details.rejected_urls[]` or `details.violations[]`, plus a `harvested` count by media type. | Compare `details.harvested` against what your schema requires. Usually a schema demanding a file the Format never makes: loosen it to nullable, or change the instruction. |
| `deliverable_missing` | The Format is declared to produce media (`io.output_kind`) and this run made none. | Retry; if it repeats, the input is not what the recipe expects. |
| `primary_output_missing` | The result satisfied your schema but left the `primary_output_key` you named empty. `output` carries the partial. | Continue the run to fill the gap, or retry. |
| `agent_reported_failure` | The run's own accepted `return_format_output` said it did not deliver: an explicit-fail payload, media slots reporting `failed` / `stand-in`, or a primary that is not the Format's declared deliverable (audio or a still under a video key). `output` carries the ledger of what was made; `primary_output_url` is null. | Read `details.reason` and `output`. The clips on `output` are real and are not regenerated by a retry; continue the run or re-fire with a new `Idempotency-Key`. |
| `incomplete_assembly` | The run reached its time limit with generation jobs still unfinished, so the delivered media is not everything it paid for. `details.pending_job_count` and `details.pending_jobs[]` name them; the partial ledger is on `output` when your schema allows it. | Continue the run with `previous_run_id`. The finished clips are on the thread and are not regenerated. |
| `output_extraction_failed` | The projection could not run. `details.reason: harvest_unavailable` means the media could not be read while the run finalized; `status` stays `completed` and the receipt fills in on the next read. `details.reason: harvest_threw` means the host's own harvest crashed after the run finished; the run is `failed`, `details.thrown` carries the throwing frame and build, and a run whose ledger holds none of the Format's declared media reports `deliverable_missing` instead. | `harvest_unavailable`: read the run once more, then retry with a new key. `harvest_threw`: a host defect — report the run id; the clips on the thread are real and are not regenerated by a retry. |
| `mcp_unavailable` | The per-turn Sume MCP tools this run needed did not attach, so the host failed it before the model ran rather than run a tool-less turn (#7378). `details.retryable` is `true` and `details.charged` is `false` — no generation ran, and nothing was billed. | Retry with a new `Idempotency-Key`. If it repeats, the tools are down, not your request. |
| `provider_unavailable` | The model's provider stream was cut and its reconnects ran out before the run produced its deliverable. Nothing about your input caused it. `details.retryable` is `true`. | Retry with a new `Idempotency-Key`; the finished clips are on the thread and are not regenerated. |
| `format_run_failed` | The generic failure. | Read `message`. A run that wanted to spend past its cap lands here, so compare `usage.billable_amount_usd_micros` with `usage.generation_spend_cap_usd_micros` before raising the brief. |

Treat this set as open: new codes may appear, so handle the ones you know and fall through on
the rest. Retry a failed run with a **new** `Idempotency-Key`; the old one is bound to the
receipt you already have. When the failure left clips behind, prefer
[continuing the run](/formats/runs#continue-a-run) over a fresh one.

Over a webhook, the same run arrives with `status: "ERROR"`, `outcome: "error"`, and
`error.code` mirroring `payload.error.code`. A run that completed but could not fill your
schema arrives as `status: "OK"`, `outcome: "degraded"`: real media, `output: null`.

## Webhook delivery failures

A delivery outcome never changes the run. The `webhook_delivery` block on every receipt says
what happened:

| `webhook_delivery.status` | Meaning | What to do |
|---|---|---|
| `retrying` | An attempt failed; `next_attempt_at` is the next one. HTTP `429`/`503` from your endpoint honour `Retry-After` up to one hour. | Nothing, unless `last_status_code` is yours to fix. |
| `failed`, `exhausted` | Ten attempts refused, timed out (10 s each), redirected, or the URL failed re-validation. `last_status_code` and `last_error` say which. | Read the receipt from `result_url`; fix the endpoint; `POST …/webhook/redeliver` to replay. |
| Envelope with `payload: null` | The receipt was over 1 MiB. `error.code` is `payload_too_large` and `error.result_url` says where to fetch it. `status` still reports the run's real outcome. | Fetch `result_url`. A handler that assumes `payload` is an object will throw on your largest runs. |

Full delivery rules: [Runs and results](/formats/runs#webhook).

## Credits and spend

Runs spend from the workspace the key belongs to. Two gates apply, at different times:

| Gate | When | On failure |
|---|---|---|
| Wallet | At create. The workspace must be able to fund the run. | `402 insufficient_credits` (`next_action: add_funds`), or `402 organization_wallet_not_provisioned`. Nothing ran. |
| Spend cap | During the run. The run cannot spend past its effective cap. | The run ends `failed`; `usage` shows how close to the cap it got. |

The cap is the control you own. Every Format carries one (`generation_spend_cap_usd_micros`
on the Format; $400 when it never set one), and `generation_spend_cap_usd` on the request
names this run's own ceiling up to the $500 platform maximum. Above the Format's cap is
honored; `null` runs at $500; `0` is rejected. Production long-form runs are typically created
with caps around $120, a single-scene retry with a few dollars. Details:
[Spend caps](/formats/call#spend-caps).

What you are charged for is metered generation (video, image, avatar, voice, timeline work) at
the rates on the [API pricing page](https://www.sume.com/pricing/api). The receipt reports it
as `usage.billable_amount_usd_micros`, which climbs while the run is in flight, counts both
reserved and captured amounts, and settles when the run terminates. It excludes the agent's
own LLM turn, so it is not the run's total cost, and it is a receipt figure rather than an
invoice: [`GET /v1/usage`](/dashboard/usage) and `GET /v1/balance` are the billing records.
`usage` is `null` when spend could not be read, which is different from `0`.

Generation that finished before a cancel or a failure is billed; a later step failing does not
refund it. A `4xx` at create, an idempotent `200` replay, and a `skipped` run cost nothing.

## Rate limits

Every key has a per-minute request budget across all of `/v1`, set by the workspace's plan.
Reads and writes have separate budgets, and reads get forty times the write number, so
polling cannot starve your own creates.

| Plan | Writes per minute | Reads per minute |
|---|---:|---:|
| Free | 120 | 4800 |
| Pro | 300 | 12000 |
| Startup | 600 | 24000 |
| Scale | 1200 | 48000 |
| Enterprise | Contracted; Scale until provisioned | Contracted |

A **read** is any `GET`: the receipt, `status_url`, `events_url`, `result_url`, Format and run
lists. A **write** is everything else: creating runs and queues, cancel, redeliver.

Every response carries the current state, and a `429` names the budget it came from:

| Header | Meaning |
|---|---|
| `ratelimit-limit` | Requests allowed in the current window, for the budget this request spent from. |
| `ratelimit-remaining` | Requests left in that window. |
| `ratelimit-reset` | Seconds until the window resets. |
| `retry-after` | Seconds to wait, sent on `429`. `error.details.scope` is `read` or `write`. |

Pace on the headers rather than counting requests yourself. Request rate is not generation
capacity: how many generations run at once is governed by the plan's concurrency limit, and
raising your request rate does not raise it.

## Getting help

Every response carries `x-sume-request-id`, and every receipt carries `request_id`. Quote
them, the run id, and the `error.code` when you write in. Do not send API keys, signing
secrets, or raw media URLs.

## Next

- [Create a run](/formats/call): the create contract, including the `401` / `403` / `404` dialect in detail
- [Runs and results](/formats/runs): polling, webhooks, `webhook_delivery`, continuing a failed run
- [Structured output](/formats/structured-output): schema rules and every `output_error`
- [Authentication](/authentication): keys, rotation, and the full rate-limit notes
