# Errors

Decisions are not errors. `DENIED`, `APPROVAL_REQUIRED` and `NEEDS_INFORMATION` are `200 OK` with a `decision`.
HTTP errors mean the request itself could not be processed.

## Envelope

```json
{ "error": { "code": "idempotency_key_required",
             "message": "An Idempotency-Key header is required for binding authorizations.",
             "request_id": "req_396516089a9549efa820a7b6",
             "details": {},
             "retryable": false } }
```

Branch on `error.code`, never on `message`. Quote `request_id` when asking for help.

## The errors you will actually hit

| HTTP | `code` | when | fix |
|---|---|---|---|
| 401 | `missing_api_key` | no `X-API-Key` / `Authorization: Bearer qtrn_sk_...` (an owner session token is not an agent key) | `details.how_to_fix`, `details.where_to_get_one` |
| 401 | `invalid_api_key` | malformed (`details.expected_format: qtrn_sk_key_<id>_<secret>`) or unknown key | copy the full key exactly as issued; issue a new one under *Agents* |
| 401 | `credential_revoked` / `credential_expired` | the key was revoked / expired | issue a new key |
| 403 | `forbidden` / scope errors | the credential lacks the scope (e.g. `approvals.read`) | grant the scope on the credential |
| 422 | `validation_error` | body field invalid - `details.fields[]` `{field, problem, type}` + `corrective_action` (e.g. `amount_minor: Input should be a valid integer` for `"19.99"`) | integer minor units, correct types |
| 422 | `idempotency_key_required` | `mode: binding` (or an execution POST) without the header | add `Idempotency-Key` (8+ chars) |
| 422 | `invalid_action_type` / `invalid_execution_mode` / `invalid_commitment` / `invalid_action_class` | enum out of range - `details.supported` | use a listed value (`GET /authorize/schema`) |
| 404 | `not_found` | unknown or another agent's `authorization_id` (`details.authorization_id`) | check the id |
| 409 | `idempotency_key_reuse` | same key, different body | new key for a new request |
| 409 | `idempotent_request_in_progress` | same key, first call still running (`retryable: true`) | wait, retry same key |
| 409 | `outcome_already_recorded` | second `/outcome` | read `GET /authorizations/{id}` |
| 409 | `authorization_not_executable` | `/outcome` on `REVOKED` / `EXPIRED` / `DENIED` / `APPROVAL_PENDING` | authorize again |
| 429 | `rate_limited` | per-credential (120/min default) or public rate limits | wait `details.retry_after_seconds` |
| 200 | `decision: NEEDS_INFORMATION`, `needs_information[0].code: wallet_missing` | the agent has no granted authority yet | Golden Path step 2 |

## Owner-API errors on the Golden Path

| HTTP | `code` | when |
|---|---|---|
| 422 | `validation_error` "Specify approve: true or false" | `POST /approvals/{id}/respond` without `approve` |
| 422 | `consent_required` | first marketplace purchase before accepting the Marketplace Buyer Terms (`/app/legal`) - execution adapters only |
| 401 | `unauthenticated` | expired owner session - `POST /auth/login` again |

## MCP

Tool errors are returned as MCP tool errors whose text starts with the same code: `unauthorized: ...`, `forbidden: this
credential is missing the 'approvals.read' scope ...`, `rate_limited: ...`. Decisions are tool **results**, not errors.

## Rate limits

| surface | limit |
|---|---|
| agent credential (REST + MCP) | 120 requests / minute (per credential; configurable by the owner) |
| `POST /auth/login`, `/auth/signup` | strict per-IP limits |
| public endpoints (`/public/*`, `/authorizations/verify`) | 20-60 / minute / IP |

A `429` carries `details.retry_after_seconds`. Denials are never rate limits.
