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
| Code | Status | What it means |
|---|---|---|
unauthenticated | 401 | Missing, malformed, expired or wrong-book token |
forbidden | 403 | You can see it; your role cannot do this |
not_found | 404 | No access at all — deliberately not a 403 |
validation | 422 | The request body is not the right shape |
ledger_rejected | 422 | The ledger refused it. The book is unchanged |
conflict | 409 | The state does not allow it |
no_book | 409 | The book has not been provisioned |
book_paused | 409 | The book is suspended; waiting will not fix it |
rate_limited | 429 | Slow down |
internal | 500 | Ours |
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.