Double-entry, balanced per currency
Every transaction is a set of postings that move together. A posting names an account, an amount, a currency, and a direction (debit or credit). The ledger accepts a transaction only when its debits equal its credits in every currency it touches:
unbalanced (422).
Accounts carry one of five accounting types: asset, liability, revenue, expense, or equity. The type is informational for reporting; the balance rule applies regardless of type.
Integer minor units
Amounts are integers in the currency’s smallest unit: cents forUSD, satoshis for BTC, the base unit for a stablecoin. Floats are rejected at the schema. This removes rounding ambiguity from the wire format. Each response reports currency_decimals so you can format the value for display.
The ledger does not police the currency symbol. Post in one it does not recognize and the write succeeds, but the response carries unknown_currency: true and a null currency_decimals, which means nothing can format it for you. Display the integer as it stands until you know the scale.
Append-only and corrections
Postings are never edited. The economic columns of a posting (amount, direction, currency, account) are immutable once written. You do not “fix” a transaction in place; you post a new one that offsets it. There are two correction primitives:- Reversal undoes a transaction in full.
POST /v1/transactions/{id}/reversebooks an inverse transaction that references the original. The new transaction links back viareverses, and the original gains areversed_bypointer. - Refund offsets a transaction partially or fully.
POST /v1/transactions/{id}/refundbooks an inverse for the requestedamount, proportionally scaling each leg so the result still balances per currency. Scaling uses banker’s rounding and puts any residual minor unit on the largest leg, so a refund of a third never leaks a cent.
status to archived. It stays readable and still counts toward balances alongside its reversal; archived only tells you it has been undone.
Transaction metadata is the one mutable surface, changed with PATCH /v1/transactions/{id}, which merges by default. Pass merge: false to replace the object. The postings themselves stay frozen. There is no delete: DELETE /v1/transactions/{id} answers 405.
Because history is append-only, the events tail is your audit log. Nothing is silently rewritten, so you can always replay what happened.
Idempotency
Every write accepts anIdempotency-Key header. The key, scoped to your ledger, maps to exactly one transaction forever. Replaying a request with the same key returns the original transaction with a 200 and an Idempotent-Replayed: true header, instead of writing a duplicate. Keys match ^[A-Za-z0-9_:.-]+$ and are capped at 255 bytes.
missing_idempotency_key (422).
To validate a transaction without writing it, pass ?dry_run=true. A dry run needs no idempotency key, emits no events, and changes no balances; it returns a summary telling you whether the postings balance.
Multi-currency
Currency is a first-class primitive, not an afterthought. Account currency is chosen at creation and immutable. A transaction may touch several currencies at once; the balance rule applies independently per currency. When a transaction spans more than one currency (for example moving value through an FX leg), the ledger records the exchange-rate snapshot on the transaction so the rate is preserved for audit and replay. Downstream reports read that recorded rate rather than recomputing from current market data. Operations that need a per-currency contract take acurrency argument explicitly. A refund of a multi-currency transaction requires currency to disambiguate which leg you are refunding; omitting it returns refund_currency_required (422). The platform never silently falls back to “the original’s currency”.
Value-date semantics
Each posting has avalue_date: the instant the movement is economically effective. Point-in-time balance and statement queries use value_date, not the time the row was inserted.
Read a historical balance by passing ?at=<ISO 8601 timestamp> on the balance endpoint:
value_date. Account statements work the same way: GET /v1/accounts/{id}/statement?from=...&to=... returns the opening balance, every posting in the window ordered by value_date, and the closing balance.
Next steps
Pagination and expand
Cursor pagination and inlining related objects.
Webhooks
Event delivery, signatures, and replay.
Errors
Every error code, with hints.
API reference
Every endpoint, generated from the spec.