> ## Documentation Index
> Fetch the complete documentation index at: https://docs.impellabs.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> The error envelope, the seventeen codes, and why `blocked` is not `failed`.

Every failure on `/api/v2/` is the same shape.

```json theme={null}
{
  "error": {
    "code": "usage_blocked",
    "message": "This workspace cannot run AI requests right now.",
    "request_id": "req_01J8Z…"
  }
}
```

<Warning>
  **Switch on `code`, never on `message`.**

  `code` is a closed vocabulary. `message` is prose written to be shown to a
  person, and it may be reworded without a version bump.
</Warning>

`request_id` is what to quote in a support ticket. It is also on the
`X-Request-Id` response header, **including on responses that carry no body**.

A failure that ends a run which had already started carries three more fields
beside the error object — `id`, `status` and `trace_id` — so you can tie the
failure to the run it ended:

```json theme={null}
{
  "error": { "code": "structured_output_failed", "message": "…", "request_id": "req_…" },
  "id": "run_…",
  "status": "failed",
  "trace_id": "trc_…"
}
```

Some endpoints add one extra key naming the resource the failure was about. Treat
the error object as open.

## The codes

| Code                       | HTTP    | Run status    |
| -------------------------- | ------- | ------------- |
| `invalid_request`          | 400     | `failed`      |
| `authentication_error`     | 401     | `failed`      |
| `usage_blocked`            | **402** | **`blocked`** |
| `permission_denied`        | 403     | `failed`      |
| `invalid_context`          | 403     | `failed`      |
| `agent_not_found`          | 404     | `failed`      |
| `conversation_not_found`   | 404     | `failed`      |
| `run_not_found`            | 404     | `failed`      |
| `agent_not_available`      | 409     | `failed`      |
| `idempotency_conflict`     | 409     | `failed`      |
| `structured_output_failed` | 422     | `failed`      |
| `rate_limited`             | **429** | **`blocked`** |
| `internal_error`           | 500     | `failed`      |
| `not_implemented`          | 501     | `failed`      |
| `tool_execution_failed`    | 502     | `failed`      |
| `provider_unavailable`     | 502     | `failed`      |
| `knowledge_unavailable`    | 503     | `failed`      |

## `blocked` is not `failed`

Two of those seventeen are **decisions rather than faults**, and both end a run as
`blocked`:

|                 |     |                            |
| --------------- | --- | -------------------------- |
| `usage_blocked` | 402 | The workspace cannot spend |
| `rate_limited`  | 429 | Too many requests          |

Nothing went wrong upstream. There was no model failure. The platform declined to
spend, or asked you to slow down.

Filing either as `failed` is what makes an error-rate dashboard useless and a
support answer wrong — "your integration is broken" when the truthful answer is
"you are out of credits".

<Note>
  **402 is deliberate.** The two obvious alternatives are both misleading: 403
  says the caller lacks permission, which is a different fix, and 429 says slow
  down, which will never help. 402 is what a billing-gated API returns.
</Note>

For a client, the practical split is:

<AccordionGroup>
  <Accordion title="Retry after a delay">
    `rate_limited` (429) — honour any `Retry-After`.

    `provider_unavailable` (502), `knowledge_unavailable` (503) — transient.
    Back off.
  </Accordion>

  <Accordion title="Do not retry; a human has to act">
    `usage_blocked` (402) — retrying will never help. Top up or raise the cap.

    `permission_denied` (403) — the key is missing a scope. The message names it.

    `invalid_request` (400), `structured_output_failed` (422) — the request or the
    schema has to change.
  </Accordion>

  <Accordion title="Retry only if it is safe">
    `internal_error` (500), `tool_execution_failed` (502).

    For anything that writes to your systems, read
    [the unknown-outcome rule](/v2/jobs#when-the-outcome-is-genuinely-unknown)
    first. A timeout on a write is not a failure — it is an unknown.
  </Accordion>
</AccordionGroup>

## Run statuses

A run ends in one of four terminal statuses:

| Status      | Means                          |
| ----------- | ------------------------------ |
| `completed` | It worked                      |
| `failed`    | Something went wrong           |
| `cancelled` | It was told to stop            |
| `blocked`   | The platform declined to spend |

`queued` and `running` are in flight, and a caller polling one may see it change.

`cancelled` carries **no error object**. Attaching a fault to a stop would put it
back into every error-rate panel that counts one, which is exactly what giving it
its own status was meant to avoid.

## Nothing internal leaks

`message` is the only text that reaches you, and it defaults to a fixed per-code
string. The diagnostic detail — the exception, the id that was guessed, the
provider that timed out — is logged against `request_id` and is not serialised.
There is no field that carries it outward.

So a `message` never contains a stack trace, a provider name, a database id, or
any prompt text.

<Warning>
  **A resource in another workspace and a resource that does not exist answer 404
  identically, byte for byte.** There is no 403 that confirms existence.

  A 403 would turn id-guessing into an inventory. This rule does not soften at
  the public boundary — it gets stricter, because this is where the guessing
  happens.
</Warning>

The one place this reads oddly is `preview`: a caller without `agent:preview`
asking to run a draft gets **404, not 403**. Confirming that a draft exists but may
not be run is exactly the confirmation the rule exists to withhold.

`invalid_context` is a **403 and not a 422**, for a related reason. A request
context naming something you may not reach is an authorization refusal wearing a
payload's clothes. It is not an existence oracle either: the check is set
membership in your own grants, so a resource that does not exist and one that
exists elsewhere answer identically.

## Three codes you may not expect

| Code                   | Why it exists                                                                                            |
| ---------------------- | -------------------------------------------------------------------------------------------------------- |
| `run_not_found`        | A generic 404 would make "no such run" and "no such agent" indistinguishable in a client's error handler |
| `idempotency_conflict` | The request is fine; the **key** is not. That is not `invalid_request`                                   |
| `not_implemented`      | So a client can tell "wrong URL" (404) from "not built yet" (501)                                        |

## Errors elsewhere in the platform

The `/api/v1/` surface and the [Reseller API](/reseller/errors) predate this
envelope and do not all use it. Everything under `/api/v2/` does.
