Error Codes
Every error carries a stable code, a sentence, and — for anything the ledger
refused on accounting grounds — a details object holding the figures involved.
{
"error": {
"code": "UNBALANCED",
"message": "Debits and credits must be equal. Debits total 5000000, credits total 4500000, a difference of 500000.",
"details": {
"total_debit": 5000000,
"total_credit": 4500000,
"difference": 500000,
"entry_count": 3
}
}
}
Read details, not the message. The message is prose that may be reworded;
code and the keys in details are the interface. The difference on an
UNBALANCED error is usually the line that was omitted or mistyped.
Errors never suggest a correction. The ledger states what is wrong and stops there. A caller that got the books wrong should not be handed a plausible-looking fix to apply unread — that turns one bad entry into a confidently wrong one.
Status codes
| Status | Meaning | What to change |
|---|---|---|
| 400 | The request itself is malformed | The syntax — bad JSON, a missing field, a date in the wrong format |
| 401 | Bad or missing credentials | The headers |
| 403 | Key lacks the required scope | The key |
| 404 | No such account or transaction in this organization | The id |
| 409 | A conflict with something protected | Nothing — the operation is not allowed on that row |
| 422 | Well-formed, but refused on accounting grounds | The numbers |
The 400 / 422 split is the useful one. A 400 means the request was never comprehensible. A 422 means it was understood and would have broken the ledger.
Ledger error codes
Balance and entries
| Code | Status | Raised when |
|---|---|---|
UNBALANCED | 422 | Debits do not equal credits on a transaction being posted |
NO_ENTRIES | 422 | A posted transaction has fewer than two entries |
INVALID_ENTRY_AMOUNT | 422 | An entry has both sides non-zero, both zero, a negative amount, or a decimal |
details for UNBALANCED carries total_debit, total_credit, difference
and entry_count. For INVALID_ENTRY_AMOUNT it carries entry_index, field
and received, so a forty-line transaction tells you which line is wrong.
Amounts are integers in minor units. 150000 is ₹1,500.00. A decimal is
refused rather than rounded — rounding would silently move money, and you are
better placed than the ledger to decide what the figure should have been.
Accounts
| Code | Status | Raised when |
|---|---|---|
ACCOUNT_NOT_FOUND | 404 | The account does not exist in this organization |
GROUP_ACCOUNT_NOT_POSTABLE | 422 | An entry names a group heading; post to one of its children |
ACCOUNT_NOT_POSTABLE | 422 | The account is quarantined |
ACCOUNT_HAS_ENTRIES | 422 | Making an account a group, or deleting it, when entries point at it |
INVALID_PARENT | 422 | The parent does not exist, is not a group, is a different type, or the move would form a cycle |
SYSTEM_ACCOUNT_PROTECTED | 409 | Changing the type or source_ref of a seeded system account |
An account belonging to another organization returns ACCOUNT_NOT_FOUND, the
same as one that does not exist — so the two cannot be told apart by comparing
responses.
Transaction lifecycle
| Code | Status | Raised when |
|---|---|---|
TRANSACTION_IMMUTABLE | 409 | Editing or deleting a transaction that is posted or void |
INVALID_STATUS_TRANSITION | 422 | A status move the lifecycle does not allow |
INVALID_STATUS_TRANSITION carries current_status, requested_status and
allowed, so the legal moves come back with the refusal.
Request-level
| Code | Status | Raised when |
|---|---|---|
VALIDATION_ERROR | 400 | A required field is missing or malformed |
INVALID_JSON | 400 | The body is not a JSON object |
Handling errors
Branch on code. It is stable across releases; the message is not.
response = post("/api/v1/transactions", json=payload)
if response.status_code == 422:
error = response.json()["error"]
if error["code"] == "UNBALANCED":
# details["difference"] is what the entries are short by
difference = error["details"]["difference"]
...
elif error["code"] == "GROUP_ACCOUNT_NOT_POSTABLE":
# details["entry_index"] identifies the offending line
...
A 422 is never worth retrying unchanged — the same request will be refused identically. A 5xx is.