Errors and what a book refuses

ShippedWorks today.

Every refusal arrives in one shape with a code, and one of those codes — ledger_rejected — carries a promise: the book is unchanged.

The envelope

{ "error": { "code": "ledger_rejected", "message": "..." } }

One shape, always. The message is for a human — usually bean-check’s own words, passed through rather than paraphrased.

The codes

CodeStatusWhat it means
unauthenticated401Missing, malformed, expired or wrong-book token
forbidden403You can see it; your role cannot do this
not_found404No access at all — deliberately not a 403
validation422The request body is not the right shape
ledger_rejected422The ledger refused it. The book is unchanged
conflict409The state does not allow it
no_book409The book has not been provisioned
book_paused409The book is suspended; waiting will not fix it
rate_limited429Slow down
internal500Ours

ledger_rejected is the one to build against. Its documented meaning is that nothing was written — the working tree was restored and no commit was made — so a caller can retry a corrected payload without checking what landed first.

Why a timeout is a 500 and not a 422

If bean-check itself hangs and is killed, the book is genuinely untouched, so 422 would not be lying about state. It would still be the wrong answer: ledger_rejected means your directives were rejected, and that sends you off to fix input that was already correct. Our validator hanging is our failure. The rare case where a rollback itself fails is also a 500, for the opposite reason — it is the one case where “the book is unchanged” cannot be promised, and it must not be dressed as a clean rejection.

What the validator actually rejects

Postings that do not sum to zero. The most common one, and the point of the whole exercise.

An account that was never opened. Open it first — see accounts.

A currency the account is pinned to reject. open Expenses:Foo USD refuses a EUR posting with “Invalid currency”, and there is no tool to amend an existing open.

A date that is not a date. A form we do not recognise is refused rather than guessed at, which is the safe direction to be wrong in.

What a book refuses before it validates

A book accepts ledger entries, not configuration. Only dated directives get through: open, close, commodity, balance, pad, note, document, price, event, and transactions. An option, plugin, include or custom line is refused by name, with the book’s bytes and its git history unmoved.

This is an allow-list on purpose. The text arriving at a book was composed by a language model out of a customer’s prose, and a single plugin or option "insert_pythonpath" line can change what validation means — which would turn the whole validate-then-commit pipeline into a formality. An include is worse and more boring: it is honoured from an included file, so it makes a book’s validity depend on a file no write of ours validated, no commit of ours records, and revert cannot put back.

What there is not

No error catalogue you can fetch. No public OpenAPI document, no machine-readable code list — this table is it.

No rate-limit headers you can plan against. 429 exists; a budget you can read does not.

No warnings channel. A write either commits or is rejected. There is no third state where something lands flagged as suspicious — the closest thing is the ! flag the agent puts on an entry it was not confident about, and that is content in your ledger, not a system signal.

Something here wrong or missing? Put it on the board — it is public, and the reply is in the thread.