---
title: Model errors
description: "What a refused or failed generation tells you: the problem object, the ten typed codes, which ones refund, and what to do next."
---

# Model errors

A refused submission returns an HTTP problem. An accepted generation can fail later, in which case the job remains readable and explains the failure. Read the machine-readable code to choose an action and the recorded refund outcome to tell the customer what happened to their credits.

![Errors](../assets/art/errors.jpg)

## The problem object

The API uses the RFC 7807 `Error` object. On a submit refusal, `code` is a field of that problem body; on an accepted job, the typed code is `failure.code`. An HTTP `200` from `GET /jobs/{id}` can contain a job whose `status` is `failed`.

<!-- gen:fields schema=Error -->
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `code` | string | No | Machine-readable error code.… |
| `type` | string | Yes | A URI reference identifying the problem type. |
| `title` | string | Yes |  |
| `status` | integer | Yes |  |
| `detail` | string | No |  |
| `instance` | string | No |  |
| `job_id` | string | No | The job this refusal points at, so a client can follow it without reading `detail`.… |
| `request_id` | string | No |  |
<!-- /gen -->

This is the production `400` response for an unknown image model, captured on 2026-09-21.

```json title="400 Bad Request — production response"
{
  "code": "validation",
  "detail": "unknown image model",
  "request_id": "localhost/7bnUUXx7b2-007287",
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}
```

| Field in this response | Meaning |
| --- | --- |
| `code` | `validation`: change the request before submitting again. |
| `detail` | Human-readable explanation; do not parse it for application logic. |
| `request_id` | Identifier to include with a support report. |
| `status` | The HTTP refusal status, `400`. |
| `title`, `type` | The problem's short title and type URI. |

## Guidance

Branch on `code`, never on `detail` or `failure.message`. Show the explanation to the customer and retain `request_id` for support. Treat an unfamiliar generation code as `job_failed`; if no code is supplied, use the status and structured fields such as `job_id`.

For a failed job, `failure.credits_refunded: true` means the hold was released and the job cost nothing. `false` means charged. An absent or null value does not prove either outcome: the hold may still be settling, no hold may exist, or the job may predate the field.

<!-- gen:fields schema=JobFailure -->
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `code` | `GenerationErrorCode` | No | Stable refinement of `kind`, present on every job that failed after this field shipped and derived from the recorded reason for older ones.… |
| `kind` | string | Yes | What stopped the job.… |
| `message` | string | Yes | Human-readable reason, safe to show the customer as-is. The same text as `error.detail`. |
| `credits_refunded` | boolean, nullable | No | What the credit ledger actually did with this job's credit hold, recorded when the hold settled.… |
<!-- /gen -->

> [!WARNING]
> `401` means authentication is missing, invalid or expired. `402` with `out_of_credits` means the wallet cannot pay; a separate `402 Upgrade Required` can mean the model needs a higher plan. Replacing a valid token does not fix a balance or plan refusal.

There is no separate runner-error taxonomy or error-type response header; use the problem status and code, or the job's typed failure.

## Error codes

These eleven values come from the generation contract. The sections below distinguish an HTTP refusal from a code on a failed job; an upstream status inside `job.error` is not the status of your successful job read.

<!-- gen:enum schema=GenerationErrorCode -->
| Value | Meaning |
| --- | --- |
| `out_of_credits` | the wallet cannot pay for the job. Nothing was submitted and nothing was charged. Top up, or submit a cheaper model or fewer seconds. |
| `rate_limit` | refused for now, not refused outright - the caller's own concurrency ceiling, or the provider's. Retry later; the same request will be accepted. |
| `prompt_nsfw` | a content filter refused the request or the result it produced, on safety grounds. Editing the prompt or the reference media is the fix. Whether the credits were refunded is `failure.credits_refunded`, not this code. |
| `ip_detected` | a content filter refused it for a real person's likeness or a protected work, rather than for safety. The fix is different from `prompt_nsfw` - change the reference image or the named subject, not the tone of the prompt - which is why it is its own code. |
| `job_failed` | the job ran and did not produce an asset for any other reason, including a provider error. The default for an unclassified failure. |
| `timeout` | the job ran past its time budget, or a `GET /jobs/{id}/wait` returned before the job reached a terminal status. On a failed job the work is over; on a `wait` the job may still be running. |
| `validation` | the request itself is not acceptable - a missing or malformed field, a model that does not support the capability asked of it, a duration the model will not render. Nothing was submitted and nothing was charged. Retrying unchanged will fail identically. |
| `confirmation_rejected` | the cost confirmation gate did not pass. Either a client showed the customer a quote and the customer declined, or a supplied confirmation token was refused. A submit that carries no confirmation token is never refused for this reason. |
| `canceled` | the job was canceled by its owner (`POST /jobs/{id}/cancel`), not broken. It is the code the SDK wait helpers raise for a `canceled` job; `cancellation` on the job says what the provider did and what was refunded. A canceled job carries no `failure`. |
| `job_not_cancellable` | `POST /jobs/{id}/cancel` refused (`409`) because the job already finished, or its finished result is already being delivered. The job will reach `succeeded` or `failed` on its own; nothing was changed. |
| `approval_required` | refused with `402` (title `Approval Needed`) because the generation would take an agent run past the price its customer approved (the `credit_ceiling` a guided preset's review card showed, sent with the brief). Nothing was submitted and nothing was charged. The detail names the generation's cost and the new run total; the agent must ask the customer to approve that total before it continues, never retry, split the job or switch models to fit. |
| `run_ended` | refused with `409` (title `Run Ended`) because the agent run this generation was started for (the turn its turn-scoped credential names) has already ended: the platform failed, swept or stopped the turn, or it finished and delivered its reply. Nothing was submitted and nothing was charged. The agent must stop working on that run, never retry or switch models; the customer's chat already shows how it ended. |
<!-- /gen -->

### out_of_credits

The available wallet balance or organization member budget cannot cover the generation. The server refuses the request before starting billable work.

| Property | Value |
| --- | --- |
| HTTP status | `402 Payment Required`; `402 Budget Exceeded` for an organization member budget. |
| Refund | No refund needed: nothing submitted and nothing charged. |
| Retry unchanged? | Only after adding credits or restoring budget. |
| What to do | Check the spendable balance, top up, or quote a cheaper model or shorter duration. |

```json title="402 — example from the Error schema"
{
  "type": "about:blank",
  "title": "Payment Required",
  "status": 402,
  "detail": "The available credits cannot cover this generation.",
  "code": "out_of_credits"
}
```

| Field | Meaning |
| --- | --- |
| `type`, `title`, `status` | Problem metadata; the request was refused with `402`. |
| `code` | `out_of_credits` distinguishes the wallet refusal from another payment requirement. |
| `detail` | Explanation for the customer; this schema-built example is not a production capture. |

### rate_limit

The caller has reached a generation concurrency ceiling or quota, or capacity is temporarily unavailable. A refused submit has no new job; an already accepted job waiting on provider capacity keeps its existing id.

| Property | Value |
| --- | --- |
| HTTP status | `429 Too Many Requests`. |
| Refund | No refund needed for a local concurrency or quota refusal: no new hold. |
| Retry unchanged? | Yes, after capacity becomes available or the quota resets. |
| What to do | Honour `Retry-After` when supplied and `Retry-After-Reset` on quota refusals; otherwise use backoff. |

The example uses the handler's detail for two active generations on a two-slot plan.

```json title="429 — example from the Error schema"
{
  "type": "about:blank",
  "title": "Generation Concurrency Limit Reached",
  "status": 429,
  "detail": "You have 2 generations running, which is the concurrent maximum of 2 for your plan. Wait for one to finish or upgrade your plan to run more at once.",
  "code": "rate_limit"
}
```

| Field | Meaning |
| --- | --- |
| `type`, `title`, `status` | Problem metadata; the submit was refused with `429`. |
| `code` | `rate_limit` signals a temporary refusal. |
| `detail` | Active count and ceiling in this example. Read live counts from `GET /me`, not by parsing this sentence. |

See [Concurrency limits](./concurrency-limits.html) for the limits and [Platform headers](./headers.html#response-headers) for reset timing.

### prompt_nsfw

A provider's safety filter refused the input or blocked the result it generated. The name does not mean the prompt alone caused it: reference media and output moderation can also trigger this code.

| Property | Value |
| --- | --- |
| Where it appears | `failure.code` on a failed job; also an HTTP problem for an immediate refusal. |
| HTTP status | `400` for a remembered policy refusal repeated locally; `422` when an immediate upstream content-policy rejection reaches the submit error handler. |
| Refund | Yes, unless the provider billed the refused attempt. A refusal whose provider answer shows no billable work, or says nothing either way, is refunded. GPT Image refusals are always refunded, including a block of the generated image, because OpenAI bills only the images it delivers. Read `failure.credits_refunded`. A local repeat refusal costs nothing. |
| Retry unchanged? | No automatic retry; change the relevant prompt or media. |
| What to do | Check whether the explanation identifies the input or generated output, then revise that part. |

```json title="Failed-job excerpt — example from JobFailure, refunded input refusal"
{
  "status": "failed",
  "failure": {
    "kind": "moderated",
    "code": "prompt_nsfw",
    "message": "Your request was rejected by the provider's safety system. Your credits were not charged: the provider refused this request before it did any billable work. Edit your prompt or reference image and try again.",
    "credits_refunded": true
  }
}
```

| Field | Meaning |
| --- | --- |
| `status` | The job is terminal; this excerpt is not a complete `Job`. |
| `failure.kind`, `failure.code` | A moderated job, narrowed to a safety refusal. |
| `failure.message` | Customer-facing explanation from the refunded refusal path. |
| `failure.credits_refunded` | `true` confirms a release in this example; do not infer it from the code. |

> [!NOTE]
> The server remembers policy refusals for a default 30-minute window. Repeating the same request, or the refused reference set on the same model, can return `400` with the same moderation code without calling the provider or charging again. A fresh `Idempotency-Key` does not bypass that content check.

### ip_detected

A content filter identified a recognizable person's likeness or a protected work. Changing a safety-related word in the prompt may not fix a refusal of the reference image or named subject.

| Property | Value |
| --- | --- |
| Where it appears | `failure.code` on a failed job; also an HTTP problem for an immediate refusal. |
| HTTP status | `400` for a remembered likeness refusal repeated locally; `422` when an immediate upstream likeness rejection reaches the submit error handler. |
| Refund | Yes, unless the provider billed the refused attempt. A likeness refusal the model provider raises on the finished output is always refunded. `failure.credits_refunded` is authoritative. |
| Retry unchanged? | No automatic retry. |
| What to do | Change the reference image or named subject and follow the provider-specific explanation. |

```json title="Failed-job excerpt — example from JobFailure, refunded likeness refusal"
{
  "status": "failed",
  "failure": {
    "kind": "moderated",
    "code": "ip_detected",
    "message": "The reference was refused because it looks like a recognizable real person or a protected work.",
    "credits_refunded": true
  }
}
```

| Field | Meaning |
| --- | --- |
| `status` | Terminal failure; this is a schema-built excerpt, not a captured job. |
| `failure.kind`, `failure.code` | A moderation failure specifically about likeness or protected work. |
| `failure.message` | The explanation to show; the example is shortened. |
| `failure.credits_refunded` | `true` is the refund outcome illustrated here. |

> [!WARNING]
> On some video models, the model provider can moderate famous faces on output, after you have received an accepted job. That likeness refusal is refunded: ordinary people and your own character sheets are supported, while public figures, celebrities and protected works can be refused. Do not treat every `ip_detected` on every provider as a refund guarantee; read the job's recorded outcome.

### job_failed

The generation did not produce an asset for an operational or otherwise unclassified reason. This is also the fallback for an unfamiliar generation error code.

| Property | Value |
| --- | --- |
| Where it appears | `failure.code` on the failed job, or a submit problem when submission itself fails. |
| HTTP status | Submit handler: `502` for an upstream failure, `500` for an internal failure; `422` for an upstream client or provider-billing rejection without a more specific code. |
| Job read | `GET /jobs/{id}` can still return `200`; the job's `error.status` describes its failure, commonly `502`. |
| Refund | Yes for an operationally failed job. Confirm the settled outcome in `failure.credits_refunded`. |
| Retry unchanged? | With backoff for a temporary operational failure; first check any existing job. A `422` needs its explanation addressed. |
| What to do | Keep the job id and request id, read the failure, and submit a new attempt only when the previous job is terminal. |

The message and refund below match the observed Gemini video failure used in the [Jobs guide](./jobs.html#failed). No complete failed-job production fixture was saved; this is a schema-built excerpt.

```json title="Failed-job excerpt — example from JobFailure"
{
  "status": "failed",
  "failure": {
    "kind": "error",
    "code": "job_failed",
    "message": "Video generation failed due to an internal server issue…",
    "credits_refunded": true
  }
}
```

| Field | Meaning |
| --- | --- |
| `status` | Terminal job failure. |
| `failure.kind`, `failure.code` | An operational or unclassified failure. |
| `failure.message` | Provider failure explanation, shortened here. |
| `failure.credits_refunded` | The credit hold was released. |

### timeout

Distinguish a closed HTTP wait window from an exhausted generation deadline. Only the latter ends the job.

| Property | Value |
| --- | --- |
| HTTP status | `408 Request Timeout` from `GET /jobs/{id}/wait`. |
| On the job | `failure.code: timeout` means the generation reached a terminal deadline. |
| Refund | A wait request never charges and does not settle the generation hold. A terminal generation timeout refunds it. |
| Retry unchanged? | Repeat the wait on the same job id. For a terminal timeout, decide whether to start a new generation. |
| What to do | Read the job's `status` before treating the generation as failed. |

```json title="408 — example from the Error schema"
{
  "type": "about:blank",
  "title": "Request Timeout",
  "status": 408,
  "detail": "job did not finish before timeout",
  "code": "timeout"
}
```

| Field | Meaning |
| --- | --- |
| `type`, `title`, `status` | This HTTP wait ended with `408`. |
| `code` | `timeout` describes the wait request in this example. |
| `detail` | The job did not finish inside that wait window; it may still be running. |

> [!TIP]
> A wait timeout never creates an extra charge and never cancels a submitted generation. If that generation later succeeds, its original hold is still settled normally. Continue with the same job id.

### validation

A field is missing or malformed, a model is unknown, or the chosen model does not support a requested duration, capability or field combination.

| Property | Value |
| --- | --- |
| HTTP status | `400 Bad Request`. |
| Refund | No refund needed: nothing submitted and nothing charged. |
| Retry unchanged? | No, except the observed new-model rollout case below. |
| What to do | Compare the request to `GET /models` and [Common model arguments](./model-arguments.html); correct the offending fields. |

```json title="400 — production validation response"
{
  "code": "validation",
  "detail": "unknown image model",
  "request_id": "localhost/7bnUUXx7b2-007287",
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}
```

| Field | Meaning |
| --- | --- |
| `code`, `status` | A `400 validation` refusal. |
| `detail` | The image model was not recognized. |
| `request_id` | Identifier for support correlation. |
| `title`, `type` | Standard problem metadata. |

> [!WARNING]
> For about ten minutes after an API deploy, a brand-new model id can receive `400 validation` from an instance still on the previous revision. This was observed during rollout. If the published catalog confirms the id, wait about a minute and retry; ordinary validation errors still require correcting the request. See [Reliability](./reliability.html#the-ten-minute-window-after-a-deploy).

### confirmation_rejected

The optional cost confirmation did not match the request being submitted: it expired, was invalid, belonged to another account, or the body, model or price changed. The enum also covers a client declining a quote. A submit without a confirmation token is never refused for this reason.

| Property | Value |
| --- | --- |
| HTTP status | `422 Unprocessable Entity`. |
| Refund | No refund needed: the confirmation gate runs before the credit reservation. |
| Retry unchanged? | No; obtain a fresh quote first. |
| What to do | Call `POST /jobs/cost`, show the new price, and submit that exact body with its new token before `expires_at`. |

```json title="422 — example from the Error schema"
{
  "type": "about:blank",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "this quote has expired — ask for a fresh price and try again",
  "code": "confirmation_rejected"
}
```

| Field | Meaning |
| --- | --- |
| `type`, `title`, `status` | The confirmation gate refused the submit with `422`. |
| `code` | `confirmation_rejected` means re-quote. |
| `detail` | The handler's explanation for an expired quote. |

> [!WARNING]
> Confirmation tokens live for ten minutes; use the quote's `expires_at` as the deadline. Do not remove a rejected token just to force the old quote through: re-quote and show the current price before submitting.

### canceled

The job's owner canceled it with `POST /jobs/{id}/cancel`. It is not a failure: the job carries `status: canceled` and a `cancellation` object, never a `failure`. The SDK wait helpers raise this code for a canceled job.

| Property | Value |
| --- | --- |
| HTTP status | None: it is a job state. The cancel call itself answers `200`. |
| On the job | `status: canceled`, with `cancellation.stage`, `provider_cancel`, `settlement` and `message`. |
| Refund | `cancellation.settlement`: `refunded` before the job reached the provider or when the provider stopped it without billing, `partially_refunded` when the provider bills only the part it rendered, `charged` when it finished and billed, `pending` until the provider answers. |
| Retry unchanged? | Submit a new job if you still want the result; a canceled job is never delivered. |
| What to do | Show `cancellation.message` as written, and read the job again while `settlement` is `pending`. |

### job_not_cancellable

`POST /jobs/{id}/cancel` refused because the job already finished, or the provider already finished it and the result is being delivered.

| Property | Value |
| --- | --- |
| HTTP status | `409 Conflict`. |
| Refund | Nothing changed: the job settles under the usual rules when it reaches `succeeded` or `failed`. |
| Retry unchanged? | No; the job will not become cancelable. |
| What to do | Read the job; it reaches its own terminal status shortly. |

```json title="409 — example from the Error schema"
{
  "type": "about:blank",
  "title": "Conflict",
  "status": 409,
  "detail": "This job already finished, so there is nothing to cancel.",
  "code": "job_not_cancellable"
}
```

| Field | Meaning |
| --- | --- |
| `type`, `title`, `status` | The cancel was refused with `409`. |
| `code` | `job_not_cancellable` means the job is finished or finishing. |
| `detail` | Which of the two applies. |

### approval_required

An agent run started from a guided preset carries the price its review card showed ("Up to N", sent as `credit_ceiling` with the brief). A generation that would take the run past that price is refused before anything is submitted, so the agent stops and asks the customer to approve the new total instead of spending it.

| Property | Value |
| --- | --- |
| HTTP status | `402 Approval Needed`. |
| Refund | No refund needed: nothing submitted and nothing charged. |
| Retry unchanged? | No. Ask the customer to approve the new total; do not retry, split the job or switch to a cheaper model to fit. |
| What to do | Tell the customer what the next generation makes, what it costs and the run's new total, and continue once they approve. |

```json title="402 — example from the Error schema"
{
  "type": "about:blank",
  "title": "Approval Needed",
  "status": 402,
  "detail": "This generation costs 65 credits and would bring this run to 163 credits, above the 147 the customer approved. Nothing was charged. Stop and ask the customer to approve 163 credits for this run before you continue; do not retry, split the job or switch to a cheaper model to fit.",
  "code": "approval_required"
}
```

| Field | Meaning |
| --- | --- |
| `type`, `title`, `status` | The generation was refused with `402`; the account can pay, the run's approved price cannot. |
| `code` | `approval_required` distinguishes the run price from an empty wallet (`out_of_credits`) and a plan limit. |
| `detail` | The generation's cost, the run's new total and the approved price. |

### run_ended

A generation submitted with an agent run's turn-scoped credential after the platform has already settled that run: the turn failed, the platform swept it, the customer stopped it, or it finished and its reply was delivered. The run's pod can keep working for a while after that, and anything it started now would be charged for work nobody receives as the run's reply, so it is refused before anything is submitted.

| Property | Value |
| --- | --- |
| HTTP status | `409 Run Ended`. |
| Refund | No refund needed: nothing submitted and nothing charged. |
| Retry unchanged? | No. The run is over; retrying, switching models or splitting the job is refused the same way. |
| What to do | Stop working on that run. The customer's chat already shows how it ended, and anything the run finished is in their Library; they continue by sending a new message. |

```json title="409 — example from the Error schema"
{
  "type": "about:blank",
  "title": "Run Ended",
  "status": 409,
  "detail": "This agent run already ended, so this generation was refused. Nothing was charged. Stop working on this run: do not retry, switch models or start new generations for it. The customer's chat already shows how the run ended; they can send a new message to continue.",
  "code": "run_ended"
}
```

| Field | Meaning |
| --- | --- |
| `type`, `title`, `status` | The generation was refused with `409`; the account can pay, but the run it belongs to is over. |
| `code` | `run_ended` distinguishes an ended run from the run's price (`approval_required`) and an empty wallet (`out_of_credits`). |
| `detail` | Whether the run ended or finished, and what the agent does next. |

## Agent error codes

Agent endpoints and failed turns use a separate enum. See [Agent Sessions API](./agent-api.html) for the session and turn flow.

<!-- gen:enum schema=AgentErrorCode -->
| Value | Meaning |
| --- | --- |
| `session_busy` | the session already has a turn in flight or too many turns are pending; wait or steer. |
| `access_denied` | the credential or organization role may not do this. |
| `insufficient_credits` | the wallet cannot pay for the turn. |
| `backend_error` | the agent could not be reached or failed. |
| `timeout` | the turn ran past its time budget. |
<!-- /gen -->

## Duplicate submissions

The same generation body and `Idempotency-Key`, or the same body with no key both times, is refused within five minutes with `409` and the existing `job_id`. It carries no `code`: the status and job identifier are the contract. That refusal is never billed.

```json title="409 Conflict — production response"
{
  "detail": "this exact request was already submitted as job 35b6ff8d-0c78-440b-bdb3-bfe476b56d76 less than 5m0s ago and has not been billed twice — check it with GET /jobs/35b6ff8d-0c78-440b-bdb3-bfe476b56d76. To run it again anyway, resubmit with a different Idempotency-Key header.",
  "job_id": "35b6ff8d-0c78-440b-bdb3-bfe476b56d76",
  "request_id": "localhost/7bnUUXx7b2-007291",
  "status": 409,
  "title": "Conflict",
  "type": "about:blank"
}
```

<!-- gen:fields schema=Error only=type,title,status,detail,job_id,request_id -->
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `type` | string | Yes | A URI reference identifying the problem type. |
| `title` | string | Yes |  |
| `status` | integer | Yes |  |
| `detail` | string | No |  |
| `job_id` | string | No | The job this refusal points at, so a client can follow it without reading `detail`.… |
| `request_id` | string | No |  |
<!-- /gen -->

Follow `job_id` with a job read or wait. Keep the same key on retries; use a distinct key only for a deliberate new generation of identical input. Sets have separate per-member behavior; see [Sets](./sets.html).

## Retrying safely

Retry `408` waits on the existing job. Retry temporary `429`, `502` and `503` refusals with bounded exponential backoff and jitter, keeping the same `Idempotency-Key` on a generation submit. Honour any `Retry-After` delay and `Retry-After-Reset` time. A lost response may have accepted the work, so a resulting `409` is your route back to that job.

Fix authentication, balance, access or validation problems before retrying. For a failed job, inspect `failure.code`, `failure.message` and `failure.credits_refunded` before creating another attempt. [Request errors](./request-errors.html) covers the complete HTTP status table.

:::cards
- [Request errors](./request-errors.html): Choose an action from the HTTP status and retry headers.
- [Reliability](./reliability.html): Understand server retries, deadlines and credit settlement.
- [Pricing and credits](./billing.html): Read quotes, balances and the credit ledger.
:::
