Thoughts

Reconciliation: the unglamorous backbone of every payments system

Reconciliation never makes the demo. You don’t stand up in a review and show off the job that compares two spreadsheets at 3am. It has no UI worth screenshotting, no clever algorithm, no launch tweet. And yet every serious payments system I’ve looked at has one at its core, running quietly, because it’s the only thing that answers the question that actually matters: are my books true?

I want to make the case that this boring job is load-bearing — and then be honest about why it’s harder than it looks.

What reconciliation actually is

Your system has its own record of what happened: a ledger, ideally double-entry and append-only. The outside world — your payment processor, your bank — has its record. Reconciliation is the process of comparing the two and proving they still agree, transaction by transaction, then explaining every difference.

That’s the whole idea. Two independent sources of truth, matched against each other on a schedule. Stripe’s own framing is the same: it defines payment reconciliation as matching and comparing transaction records (opens in new tab) to ensure the payments made or received are consistent with what’s recorded in your books.

Why do you need a separate process at all — why not just trust your ledger? Because your ledger only knows what your code told it. If a webhook was dropped, if a retry double-posted, if a fee was netted in a way you didn’t model, your ledger is confidently, internally consistent, and wrong. Reconciliation is the second witness. It’s the one part of the system whose entire job is to distrust the rest of it.

Why the two sides never just match

If reconciliation were “join on transaction ID, assert equal,” nobody would write essays about it. The reason it’s real work is that the processor’s view of the world is shaped by their accounting, not yours. Three mismatches show up over and over:

  • Fees get netted. You charged the customer 100. The money that lands in your bank is 97.10, because the processor took its cut before paying out. If your ledger only knows about the 100, every single payout looks wrong until you model the fee as its own entry.
  • Payouts are batched. The processor doesn’t wire you one transfer per payment. It aggregates hundreds of transactions into a single deposit. So a number in your bank statement corresponds to a set of rows in your ledger, and you have to reassemble the batch to check it. Stripe’s payout reconciliation report (opens in new tab) exists precisely to hand you that mapping — each payout broken down into the transactions it settles.
  • Timing drifts. A payment authorized today may settle tomorrow and post to your bank the day after. On any given day, some transactions are “in flight” — real, but not yet on both sides. A naive comparison flags all of them as breaks.

None of these are bugs. They’re just the seams between two systems that were never designed to be byte-for-byte identical. Reconciliation is the machinery that stitches across those seams.

The shape of a reconciliation engine

Strip away the domain noise and it’s a matching pipeline with a few honest stages:

  1. Normalize. Pull the processor’s settlement file and your own ledger into one common shape — same currency units (integer minor units, always), same notion of a transaction, fees split out as their own lines.
  2. Match. Pair each external record with an internal one, usually on a shared reference ID, falling back to fuzzier rules (amount + date + last-four) when the clean key is missing.
  3. Classify the breaks. Whatever doesn’t match is a break, and breaks come in flavors: in your books but not theirs, in theirs but not yours, matched-but-amount-differs. The flavor tells you where to look.
  4. Explain or escalate. Timing breaks age out on their own — recheck tomorrow. The rest need a human or a rule to resolve, and every resolution should post a correcting ledger entry, never a silent edit.

The subtle part isn’t the matching. It’s step 3 and 4: making unmatched items loud and giving them somewhere to go. A reconciliation system that finds breaks but has no workflow to resolve them just generates a guilt list nobody reads.

Where it pays off

I’ve argued before that exactly-once is a lie — you process at-least-once and dedupe, and you accept that duplicates and drops are normal weather. Reconciliation is the backstop I promised in that post. Dedupe catches the duplicates you anticipated. Recon catches the ones you didn’t, plus the dropped webhook, plus the fee model you got subtly wrong. It’s the seatbelt for when a bug slips past the airbag.

Here’s the trade-off, stated plainly. Reconciliation is pure cost until the day it isn’t. It builds nothing customers can see. It’s tedious to write and tedious to operate. And it is the difference between finding a discrepancy at end-of-day, when it’s a break worth a few dollars, and finding it a quarter later during an audit, when it’s a crisis worth your reputation. You are paying a small, steady tax to avoid a rare, enormous bill.

That’s usually a trade worth making. In payments, “my numbers are probably right” is not a place you get to stand. Reconciliation is how you earn the right to say they are — and how you find out fast on the days they aren’t.

Archie

← back to Thoughts