Developers · Errors

Every error, and what to do about it.

One envelope on every door. The code is the thing to branch on; the message is for a person; the two addresses are this page, at the code's own row.

400 Bad Request
{
  "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"
  }
}

This page as Markdownllms.txt

01

The envelope

An error body is one object under error: a code from the list below, its HTTP status, a message written for a person, and type and doc_url, which are one address — this page, at the code — so the body is RFC 9457-shaped without a second content type. A param names the field at fault where there is one; a reason says which kind of conflict. Every answer, error or not, carries a Request-Id header, and an error repeats it as request_id so a support ticket and a log line meet in one string.

An MCP tool answers the same sentence, with the code in parentheses, as a tool execution error — never a protocol error — so the assistant can read it back to the person.

02

The codes

A published enum: these are only ever added to, never renamed.

  1. 400bad_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.

  2. 401unauthorized

    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.

  3. 402payment_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.

  4. 403forbidden

    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.

  5. 404not_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.

  6. 409conflict

    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.

  7. 429rate_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.

  8. 500internal

    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.

03

Conflict reasons

A 409 conflict carries a reason a client can branch on. Most mean re-read and retry with fresh data, never a blind retry.

reasonStatusMeaning
idempotency_mismatch409This Idempotency-Key was already used for a different request body. A key means one change; mint a new one for a new change.
idempotency_gone409The row this key created has since been deleted, so the first answer cannot be replayed. Nothing is re-created.
idempotency_in_progress409The first request with this key is still running. Wait, then retry with the same key to get its answer.
reconciled409The 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.
superseded409The row changed since the version you sent, or the proposal's target changed since it was filed. Re-read and send again.
ceiling_exceeded202Over 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.
409 Conflict
{
  "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"
  }
}

04

When a 202 carries a reason

A token with Make changes can still answer 202 Accepted with outcome: "proposed": the change was filed in the person's review inbox instead of written, and reason says why. It is a 202 and not a 409 because the request did something — a client that treats 409 as failure would lose the proposal it had just made.

reasonMeaning
needs_personA delete from a token that was not allowed deletes. It waits in the review inbox for the person.
possible_duplicateThe 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_exceededOver a limit set on this token. The change waits in the review inbox, badged.

05

Headers

HeaderOnMeaning
Request-IdEvery responseThe id to quote in a report. An error body repeats it as request_id.
WWW-Authenticate401 and 403Bearer realm="rudopo" with error="invalid_token" or error="insufficient_scope", and the scope that was missing.
Retry-After429Seconds to wait. A write refused by a per-user write window reports that window's own wait.
RateLimit, RateLimit-PolicyEvery response past the token checkThe IETF fields for whichever of the per-token and per-user windows would refuse first, beside the older X-RateLimit-* trio.
Idempotency-ReplayedA replayed writetrue when the answer is the stored one for a key already used with this body.