Reconciliation windows
How to open a window against a bank statement or payout file, match its lines against postings, read the residual, and close the window so the period can be signed off.
Balances in Ledgerdoc are derived from entries, never stored beside them, so the ledger is always internally consistent by construction. Reconciliation asks a different question: does the ledger agree with a document produced by somebody else? A window is how you ask it.
A window has a start, an end, one or more accounts under review, and a claimed movement taken from the external statement. Once open, you attach matches; once every statement line is either matched or explicitly written off, the residual should be zero and the window can be closed. A closed window is immutable, like an entry.
Windows do not create or modify postings. If reconciliation reveals a genuinely missing transaction, you post it with POST /v2/entries and then re-read the residual. Nothing in this article writes to the ledger.
Window lifecycle
A window occupies exactly one of four states. Only closed and abandoned are terminal, and only those two are immutable.
open
Created; accepting matches. Residual recalculates on every change.
balanced
Residual is zero. Still writable — you may unmatch and revise.
closed
Signed off. Immutable. Requires a zero residual or a recorded write-off.
abandoned
Given up on. Immutable, and preserved so the attempt stays auditable.
A window moves from open to balanced automatically the moment its residual reaches zero; it moves back if you unmatch a line. Closing is always an explicit act, because closing is the assertion a human is making.
Opening a window
Supply the period, the accounts under review, and what the statement claims. The claimed_movement is the net change the external document asserts across those accounts, in minor units, using the same sign convention as entry lines.
curl -X POST https://api.ledgerdoc.dev/v2/reconciliations \
-H "Authorization: Bearer $LEDGER_KEY" \
-H "Idempotency-Key: recon-2026-06-cash-op" \
-d '{
"label": "June 2026 — operating account",
"accounts": ["cash:operating"],
"window": { "from": "2026-06-01", "to": "2026-06-30" },
"claimed_movement": 1284400,
"statement_ref": "stmt-06-2026-4471"
}'
# 201 Created
{
"id": "rec_3Kd9pX",
"state": "open",
"derived_movement": 1281900,
"claimed_movement": 1284400,
"residual": 2500,
"matched_count": 0,
"opened_at": "2026-07-02T08:41:07Z"
}
Note that the residual is populated immediately. derived_movement is computed from the ledger's own entries over the window — it is the same derivation GET /v2/accounts/:id/balance performs with an at timestamp, taken twice and subtracted. You therefore learn the size of the disagreement before you have matched a single line.
Derive the Idempotency-Key from the period and the account, as above. A retried open then returns the existing window rather than creating a second one for the same month — the same replay semantics as idempotent postings, including the Replayed: true header.
Request fields
labelstring · requiredaccountsarray<string> · requiredcash:operating. All must exist and be visible to the key. A window over several accounts sums their movements.window.fromdate · requiredYYYY-MM-DD, interpreted in the workspace region's timezone.window.todate · requiredfrom and not in the future.claimed_movementinteger · requiredstatement_refstring · optionaltoleranceinteger · optional0. Use it for known rounding on FX statements, never as a way to bury a break.Matching lines
A match links one statement line to one or more postings inside the window. Matching does not change any posting; it records that a human or a rule has accounted for that line, and it reduces the residual.
Three match shapes are accepted, and the API does not prefer one over another:
- One to one — a statement line and a single posting agree on amount and date. The common case, and the one automation should handle.
- Many to one — several statement lines net to one posting, or vice versa. Typical for processor payouts, where a day of activity arrives as one settlement.
- Write-off — a line you accept will never match, recorded with a reason. It clears from the residual but stays visible on the window forever.
curl -X POST https://api.ledgerdoc.dev/v2/reconciliations/rec_3Kd9pX/matches \
-H "Authorization: Bearer $LEDGER_KEY" \
-H "Idempotency-Key: match-stmt-06-2026-4471-l17" \
-d '{
"statement_lines": ["l17", "l18", "l19"],
"entries": ["ent_8Fq2wZ"],
"strategy": "amount_and_date"
}'
# 200 OK — residual shrinks by the matched amount
{
"id": "mat_7Vb2nQ",
"reconciliation": "rec_3Kd9pX",
"matched_amount": 2500,
"residual": 0,
"state": "balanced"
}
Because matches are themselves idempotent writes, a matching job that crashes halfway through can be re-run from the top without double-counting. Derive each match key from the statement line identity, as in the example.
Reading the residual
The residual is claimed_movement − derived_movement − matched_amount, and it is the only number that decides whether a window can close. A non-zero residual is not a failure; it is a question, and where it sits tells you which question.
| Shape of the residual | Usual cause | What to do |
|---|---|---|
Clears when you extend window.to by a day | Settlement timing | Nothing. Close the window; the entry lands in the next period |
| Small, consistent, proportional to volume | Processor fees posted net | Post the fee entry, then re-read. Do not raise tolerance |
| Matches one statement line exactly | A posting that was never written | Post it with POST /v2/entries, then match |
| Equals twice a known amount | A duplicate posting from an unsafe retry | Post a reversing entry; audit the idempotency keys upstream |
| Unexplained after all lines are matched | A genuine break | Leave the window open. Escalate rather than write off |
Raising tolerance until the window closes converts an unanswered question into a silent one. The residual is the most valuable output of the whole exercise — protect it.
Closing and abandoning
Closing is a PATCH that asserts the period is settled. It requires state: "balanced", or a residual within tolerance with a recorded write-off reason. A closed window is immutable — there is no reopen, by design.
{ "state": "closed" }— signs off the period. Records the key that closed it and the timestamp.{ "state": "abandoned", "reason": "…" }— gives up. The attempt and its residual are preserved for audit.- Any other transition returns
409 recon_state_invalidand changes nothing.
To correct a period after its window has closed, open a new window over the same dates. The two windows sit side by side in the audit trail, which is precisely what an auditor wants to see: not a revised answer, but the sequence of answers.
Errors
Every error body carries a machine-readable code and a human message. Nothing in this endpoint family writes partially — a rejected request leaves the window exactly as it was.
to precedes from, or the window ends in the future.
accounts does not exist in the chart, or is not visible to this key.
POST /v2/entries.
tolerance, or writing to a terminal window.
strategy was not manual.
Rate limits on reconciliation
Windows and matches spend the same write budget as postings: 600 writes and 3,000 reads per minute per workspace, burstable to double for 30 seconds. A month-end matching job over ten thousand statement lines therefore needs to pace itself, or batch its matches.
- Every response carries
X-RateLimit-Remaining. A429addsRetry-Afterin seconds. - Replayed idempotent requests are not counted, so a resumed job pays only for the work it actually does.
- Reading a window and its residual is a read, not a write — poll it as often as you like.
Relationship to balance derivation
It is worth being explicit about what a window does and does not add. GET /v2/accounts/:id/balance with an at timestamp already tells you what the ledger believes at any moment, and has since v2.4.0. A window adds three things on top of that derivation:
- An external claim, recorded and kept —
claimed_movementandstatement_ref. - A record of who accounted for what, in the matches, including write-offs and their reasons.
- An assertion, by a named key, that a period is settled — which is the artefact a signed-off month actually consists of.
None of it changes a single posting. That is the point: the ledger remains a record of what happened, and the window is a record of somebody checking.