# Errors

Every error code the API answers with, its status, what it means and what to do.

## The envelope

One object under `error`: `code` (branch on this), `status`, `message` (for a person), `type` and `doc_url` (one address: this page at the code), `param` where a field is at fault, `reason` on a conflict, and `request_id`, which every response also carries as the `Request-Id` header. An MCP tool answers the same sentence with the code in parentheses as a tool execution error.

```json
{
  "error": {
    "code": "bad_request",
    "message": "Send an Idempotency-Key header: one value per intended change, the same one on a retry.",
    "status": 400,
    "type": "https://rudopo.app/developers/errors#bad_request",
    "doc_url": "https://rudopo.app/developers/errors#bad_request",
    "param": "Idempotency-Key",
    "request_id": "e4212f42-ba5a-4814-85e2-e9b8d423254d"
  }
}
```

## The codes

| Status | code | Meaning | What to do |
| --- | --- | --- | --- |
| 400 | `bad_request` | The request itself is wrong: a parameter out of range, a field the body does not have, a date that does not exist, a money value finer than its currency allows, or a missing Idempotency-Key. | Read `param` for the field at fault and fix the request. Nothing was changed; retrying the same request gives the same answer. |
| 401 | `unauthorized` | No usable token. A missing one is told so; a credential that fails gets one message whichever way it failed — malformed, unknown, revoked or expired — so a stolen token learns nothing about its own state. | Check the Authorization header, then the token's row in Settings. A revoked or expired token is replaced, never recovered. |
| 402 | `payment_required` | The account has no active subscription. Reads keep working through the grace period; writes and proposals need an active plan. | Resubscribe in the app. The token and its scopes are unchanged. |
| 403 | `forbidden` | The token is real but not for this: a scope it was not granted, or a budget outside the list it was narrowed to. Only ever about your own token. | Create a token at the level and with the budgets you need; a token's level and budgets are fixed once made. The WWW-Authenticate header names the missing scope. |
| 404 | `not_found` | The id does not exist, or belongs to someone else. The two answer identically, so a token cannot be used to probe for other people's data. | Re-read the id from the list it came from. If a row was deleted since you read it, a delete has done its work. |
| 409 | `conflict` | The write cannot happen as sent. `reason` says which kind: an Idempotency-Key reused for a different body, a key whose row has since gone, a key still running, a locked reconciled row, or a row changed since the version you read. | Branch on `reason` below. Most are a re-read and a retry with fresh data, never a blind retry. |
| 429 | `rate_limited` | A window is exhausted: per IP, per token, per user, or one of the per-user write windows. | Wait the seconds in Retry-After. The RateLimit headers on every response say how much room is left before this happens. |
| 500 | `internal` | Something went wrong on our side. The message says no more than that, because a database error can carry a row's own values. | Retry once after a pause. If it persists, send the Request-Id header's value with your report; it is in the error body too. |

## Conflict reasons

| reason | Status | Meaning |
| --- | --- | --- |
| `idempotency_mismatch` | 409 | This Idempotency-Key was already used for a different request body. A key means one change; mint a new one for a new change. |
| `idempotency_gone` | 409 | The row this key created has since been deleted, so the first answer cannot be replayed. Nothing is re-created. |
| `idempotency_in_progress` | 409 | The first request with this key is still running. Wait, then retry with the same key to get its answer. |
| `reconciled` | 409 | The row is reconciled: its amount, date, direction, account and transfer destination are locked, and it cannot be deleted or uncleared. The other leg of a reconciled transfer is locked too — its amount, date and account cannot change, and it cannot be deleted — but it can still be uncleared. On an ordinary reconciled row memo, payee and category stay editable; on a transfer leg the memo does. |
| `superseded` | 409 | The row changed since the version you sent, or the proposal's target changed since it was filed. Re-read and send again. |
| `ceiling_exceeded` | 202 | Over a limit set on this token: the per-transaction amount, the daily total, rows per call or rows per day. The change was filed as a proposal instead and nothing was written. Today it only ever arrives on a 202. |

```json
{
  "error": {
    "code": "conflict",
    "message": "This Idempotency-Key was used for a different request.",
    "status": 409,
    "type": "https://rudopo.app/developers/errors#conflict",
    "doc_url": "https://rudopo.app/developers/errors#conflict",
    "reason": "idempotency_mismatch",
    "request_id": "4f13939c-440e-4743-8fc4-42ec5965c343"
  }
}
```

## When a 202 carries a reason

A Make changes token can answer 202 with `outcome: "proposed"`: the change was filed in the review inbox instead of written, and `reason` says why.

| reason | Meaning |
| --- | --- |
| `needs_person` | A delete from a token that was not allowed deletes. It waits in the review inbox for the person. |
| `possible_duplicate` | The same token wrote a row with this account, date, amount and payee in the last ten minutes under another key. The second waits in the review inbox. |
| `ceiling_exceeded` | Over a limit set on this token. The change waits in the review inbox, badged. |

## Headers

| Header | On | Meaning |
| --- | --- | --- |
| Request-Id | every response | The id to quote in a report; an error body repeats it as request_id. |
| WWW-Authenticate | 401 and 403 | Bearer realm="rudopo" with error="invalid_token" or error="insufficient_scope" and the missing scope. |
| Retry-After | 429 | Seconds to wait. |
| RateLimit, RateLimit-Policy | every response past the token check | The IETF fields for whichever of the per-token and per-user windows would refuse first, beside X-RateLimit-*. |
| Idempotency-Replayed | a replayed write | true when the answer is the stored one for a key already used with this body. |
