- Why is this balance 1023.17?
- Which transaction changed it?
- What happened between date X and date Y?
- Does it match an external system, such as an external processor or your bank statement?
Why is this balance 1023.17?
A balance is not a stored number that someone could have edited. It is derived from the account’s postings, so you can always open it up and see the entries that produced it. List the postings on the account:from, every posting in the window ordered by value_date, and the closing balance at to. The opening balance plus the postings always equals the closing balance, so the number is never a mystery.
The statement does not paginate. It takes a limit, defaulting to 500 and capped at 5000, and reports truncated: true when it hit that limit, which is the only sign that entries are missing. Check the flag, and narrow the window rather than raising the limit.
Which transaction changed it?
Every posting belongs to exactly one transaction. The postings list and the account statement both carry the transaction reference for each entry, so you can go from “this line moved the balance” to “this transaction caused it” in one hop. Fetch the transaction to see all of its legs, its metadata, and any reversal links:reverses and reversed_by links intact. Nothing disappears.
What happened between date X and Y?
The account statement is the per-account answer. It bounds the postings byvalue_date, so a point-in-time question gets a point-in-time answer: the result is computed from when each movement was economically effective, not from when the row was inserted.
Does it match an external system?
This is reconciliation proper: comparing your ledger against an external source of truth, such as an external processor or your bank statement. You bring a snapshot of external items, and the ledger matches them against your unreconciled postings.matched, unmatched_external (items with no posting to pair), and unmatched_internal (postings in the window that no external item claimed). The unmatched sets are your break list.
Every monetary value here is a JSON string of integer minor units. Parse them with a
big-integer type. On write, amount also accepts an
integer.
Runs are replayable. Items are stored as external transactions keyed by (source, external_id), so matched postings are marked once. A re-run of the same snapshot reports already-matched items in matched with their original attribution; nothing is double-marked. Fetch a past run with its full snapshot:
Durable feeds and breaks
Inline snapshots work for one-off checks. For a recurring feed, create a source and ingest rows once; they persist with a status (open, matched, ignored) and GET /v1/external_transactions?status=open is your break list across runs.
(source, external_id); re-pushed rows report replayed. Rows carrying a reference_rail/reference_kind/reference_value triple match their transaction’s posting first (strategy external_ref), before amount heuristics run.
Work the break list with POST /v1/external_transactions/:id/ignore (a row that should never match, with a reason), /match (manual attribution to postings that sum exactly to the row’s amount), and /unmatch (undo; the match row is kept as audit). Each emits an event, so webhooks can drive a breaks workflow.
See Feeds for the full workflow, the hosted inbound push endpoint, and a Stripe recipe.
For the comparison basis itself, the reports endpoints give you ledger-side totals to check against the external figures. Use the trial balance for per-currency debit and credit totals across every account:
value_date, so an “as of” figure is reproducible. It reports totals_by_currency with a residual per currency and a top-level healthy boolean, which is true only when every residual is zero. Alert on healthy, not on your own subtraction.
External systems named here, such as a processor or a bank, are your own integrations. The ledger is your system of record for what the money means; reconciliation is how you prove it agrees with the systems that moved it.
Next steps
Feeds
Durable external feeds, the break queue, and the inbound push endpoint.
Invariants
Why balances are always derivable from postings.
Webhooks
Drive a breaks workflow off
reconciliation.run_completed.API reference
Every endpoint, generated from the spec.