# Errors & problem+json

The Management API error contract — RFC 9457 problem+json with a registered type, a stable code, and a request_id.

Source: https://tokentriage.com/docs/reference/errors

---
import Callout from '../../../components/Callout.astro';

Every non-2xx from the Management API is `application/problem+json` (RFC 9457) with a
registered `type`, a stable machine-readable `code`, and a `request_id` that matches the
`TT-Request-Id` header carried on every response.

For symptom-first fixes keyed to what you'd actually search — 401 storms, `UNPRICED` rows,
silent routing — see [Troubleshooting](/docs/reference/troubleshooting).

## The envelope

- `type` — a relative on-host path (air-gap-dereferenceable), documented at
  `/tt/api/problems/<slug>`; the full registry is served at `/tt/api/v1/problems`.
- `code` — a stable string code.
- `request_id` — matches the `TT-Request-Id` response header, for correlation with the ledger
  and the audit trail.

<Callout type="key">
JSON request bodies are **strict-decoded**: an unknown field is a `400 bad_request`,
deliberately — a silently ignored field is a lie about what the server heard.
</Callout>

## Framework problems

These apply to every operation and are documented once (they're each operation's default
response):

| Status | Problem |
|---|---|
| 400 | `bad_request` |
| 401 | `unauthenticated` |
| 403 | `forbidden` (names the `required_scope` in the body and the `TT-Required-Scope` header) |
| 404 | `not_found` |
| 405 | `method_not_allowed` |
| 413 | `payload_too_large` |
| 429 | `rate_limited` (with `Retry-After`) |
| 500 | `internal` |

## Special status codes worth knowing

- **`412 precondition_failed`** — a config compare-and-swap lost the race; refetch the
  generation and retry (`If-Match`).
- **`422 restart_required`** — a restart-only config change (e.g. a `webhooks:` edit) was
  submitted; nothing was written (previews still return `200`). Also `422
  unsupported_override_class` when overriding a rate/concurrency limit per node.
- **`410 gone`** — a sunset operation (names its successor), or `410 cursor_expired` on the
  events feed (carries `oldest_seq` so you can reconcile from the audit trail).
- **`503 enforcement_unavailable`** — a `fail: closed` governance limit whose guarantee is
  uncheckable (never a fake 429; see [Governance](/docs/build/governance#fail-open-with-disclosure)).

## Guardrail boot refusals

These are not HTTP statuses. A malformed guardrail policy **refuses at boot** — the process does not
start — so that no row is ever written carrying a fallback disclosure for a policy that should not
exist. The three you are most likely to meet:

| What you wrote | The refusal |
|---|---|
| A phase a detector does not declare | `binding "code_out" binds detector "code.patterns" to phase "stream", which it does not declare (declared: [output])` |
| `on_error: closed` with no ack | `binding "pii_in": on_error 'closed' requires fail_closed_ack — a dead detector becoming an outage is a decision an operator must type out` |
| A `judge` or `sidecar` detector with no consent | `guardrails.judge.external: … It requires external: allow — the air-gap gate. Without it this tier is REFUSED` |

Also refused: `mode: simulate` in a binding (it is the dry-run report's stamp, never a policy), and
`stream_fallback: closed` without the same `fail_closed_ack`. Stock activation also rejects empty
detector lists, `rewrite`, `reask`, `stream_fallback: buffer`, sidecar bindings, raw-required external
detectors and spanless mutation combinations. Positive judge hard caps require a proven
conservative reservation price; an explicit zero cap denies every call. See
[Configuring a guard](/docs/guardrails/configuring-a-guard).

<Callout type="note">
A guard that fails at **run time** records a suspension and applies its mode and failure posture.
Monitor preserves traffic. Enforcing/open may continue; acknowledged enforcing/closed withholds
affected unreleased bytes. Read the binding, phase, reason, execution stage and delivery receipt. That is the field to alert on — see
[Evidence & alerting](/docs/guardrails/evidence-and-alerting).
</Callout>

## Governance refusal reasons

Governance refusals map a `reason` to a status: `invalid_key` → 401, `scope_violation` →
403, `budget_exceeded` / `quota_exhausted` / `rate_limited` / `concurrency_exceeded` → 429,
`enforcement_unavailable` → 503. Each is written `row == wire` — the HTTP response and the
decision-log row are stamped from the same value.

The full catalog, and the type registry, are in the rendered [API reference](/api).
