Why architecture matters here

The reason reconciliation cannot be skipped is that no single system is authoritative about money on its own. Your ledger knows your intent, but not whether the funds moved. The processor knows what it settled, but applies fees and timing rules you may model differently. The bank knows what cleared, but in aggregate batches that hide the individual transactions. Truth about a single payment only emerges from the agreement of these independent records. Reconciliation is the process that manufactures that agreement — and, crucially, flags every case where it cannot be manufactured, because an unexplained gap is either an error in your accounting or real money that is missing.

Three properties make a reconciliation system worth trusting. Completeness: every transaction on every side is accounted for — matched, or explicitly sitting in an exception queue; nothing is silently dropped. Explainability: every break has a documented cause and a resolution path, so 'the numbers are off by $412' becomes 'a $412 chargeback settled on the 30th but posts to next period'. And immutability of the close: once a period is reconciled and locked, its numbers do not silently change; corrections flow forward as new entries, so the audit trail is intact.

The trade-off you operate around is between match rate and false matches. Loose matching — pairing on amount and approximate date — clears more automatically but risks pairing two genuinely different transactions that happen to share a value, hiding a real break behind a false agreement. Tight matching — requiring exact identifiers — is safe but leaves more to manual work when identifiers are missing or mangled across systems. The engineering job is to push the automatic match rate as high as safely possible while ensuring that when the engine is unsure, it produces an exception a human resolves, never a confident wrong pairing that quietly balances the books on a lie.

Advertisement

The architecture: every piece explained

Top row: the inputs and normalization. Three independent records arrive: the internal ledger (your system-of-intent for every agent-initiated authorization, capture, refund, and fee accrual), the processor report (the PSP's settlement file, with its fees, batch ids, and reversal codes), and the bank statement (the actual clearing, often aggregated). Each has a different format, currency convention, timestamp granularity, and identifier scheme, so the first stage is the normalizer: it maps every source into one canonical transaction schema — a common set of fields, a single currency basis with explicit FX where needed, and a normalized time so that a settlement dated in one timezone lines up with a ledger entry dated in another.

Middle row: matching and its output. The matching engine runs in tiers. First exact matching on a shared reference id — the cheapest, safest pairing. Then fuzzy matching for records that should pair but whose ids differ or are absent: amount within a tolerance, dates within a window, merchant and last-four aligning. Then N:1 matching for the aggregation reality — one bank line that corresponds to a batch of many processor transactions, which the engine must sum and reconcile as a group. Everything that agrees across all three sides lands in the matched set. Everything that does not becomes an exception: a break, or an amount parked in a suspense account until it is explained.

Bottom rows: exceptions to close. Breaks are not equal, so aging and assignment routes each to an owner and starts a clock — a $5 fee discrepancy and a $50,000 missing settlement demand different urgency. Resolution clears each break down one of a few paths: adjust the ledger when your record was wrong, dispute with the processor or bank when theirs was, or write-off a small immaterial residual under policy. Once every material break is resolved, close and attest locks the period and records sign-off. The ops strip lists what you watch continuously: automatic match rate, the age distribution of open breaks, the suspense balance (which should trend to zero), and the idempotency that lets you safely re-run the whole pipeline when a corrected file arrives.

Two design choices in this layout deserve emphasis because they decide whether the system scales. The first is that matching is computed as a function of the inputs, not as a running mutation of state: given today's three files, the matched set and the exceptions are fully determined, so re-running with a corrected file simply produces a new, correct result rather than patching an old one. That functional stance is what makes idempotency cheap instead of a special case. The second is that the suspense account is a first-class part of the model, not an afterthought — every amount that cannot yet be matched has a defined home where it is visible, aged, and owned, so 'unmatched' never means 'lost'. Together these turn reconciliation from a fragile, order-dependent batch job into a repeatable computation whose correctness you can assert on demand, which is exactly what an auditor, a regulator, or a 3 a.m. on-call engineer needs when the question is 'are we sure the money is all accounted for'.

AP2 reconciliation — proving the ledger matches realitymatch internal records against processor + bank statements, resolve every breakInternal ledgerwhat we think happenedProcessor reportwhat the PSP settledBank statementwhat actually clearedNormalizercanonical schema + currencyMatching engineexact -> fuzzy -> N:1Matched setthree-way agreementExceptionsbreaks + suspenseAging + assignmentroute to ownersResolutionadjust / dispute / write-offClose + attestperiod lock + sign-offOps — match rate + break aging + suspense balance + idempotent re-runsingestingestingestfeedagreedifferageresolveclearoperateoperate
AP2 reconciliation: three independent records — internal ledger, processor report, bank statement — are normalized and matched three ways; agreements close, breaks become aged exceptions routed to owners for resolution before the period locks.
Advertisement

End-to-end flow

Follow a single day's cycle. Overnight, three files land: your ledger export of every agent-initiated payment for the day, the processor's settlement report, and the bank's statement of what cleared. The normalizer ingests all three, maps them to the canonical schema, converts a handful of cross-border transactions to the reporting currency at the day's rate, and aligns timestamps to a single settlement date. Now three comparable sets of transactions sit side by side.

The matching engine runs. The vast majority — say 97% — match exactly: a ledger capture, a processor settlement line, and a share of a bank batch all carrying the same payment reference, all agreeing on amount net of the fee the processor reported and your ledger accrued. These close automatically into the matched set and require no human attention. The remaining 3% do not line up cleanly and flow into exceptions.

Walk three representative breaks. First, a payment the ledger shows as captured for $100 but the processor settled as $97.10 — the difference is a $2.90 processing fee your ledger booked to a separate account; the engine's fee-aware matching recognizes this as an agreement once fees are considered, and it closes automatically. Second, a $250 refund the processor reports that your ledger has no record of — an agent issued a refund through a path that did not write back; this is a genuine break, assigned to an owner, resolved by adjusting the ledger to record the refund, with a root-cause ticket for the missing write-back. Third, a $4,000 bank credit with no matching processor line — a settlement that crossed the day's cutoff; it parks in suspense, and the next day's processor file contains its counterpart, at which point it matches out of suspense and the balance returns toward zero. Timing breaks are the normal breathing of the system; the discipline is that they live visibly in suspense, never lost.

Now the stress path. Suppose the processor sends a corrected file at noon because their morning batch double-counted a reversal. You must re-run reconciliation for the day without double-applying anything. This is why every stage is idempotent: transactions are keyed by stable identifiers, matches are computed as a function of the current inputs rather than incrementally mutated, and re-ingesting a corrected file supersedes the prior version rather than adding to it. The re-run recomputes the matched set and exceptions cleanly, the erroneously-created break disappears, and — because the period had not yet closed — the books simply reflect the corrected truth. Had the period already been locked, the correction would post as a forward adjustment into the next period, preserving the immutability of the closed one.

Zoom out to the control totals that guard the whole cycle, because the most dangerous break is the one that never appears as a line item. Before matching begins, the pipeline reconciles counts and sums per file: the processor report claims 48,213 transactions totalling a stated gross, and the ingested record set must agree exactly, or a block was lost or duplicated in transit. This cheap check at the door catches an entire missing batch that per-transaction matching would never notice — because transactions that were never ingested simply are not there to flag. The same logic runs at the end: the sum of matched amounts plus the suspense balance plus the day's write-offs must equal the movement the bank statement shows, a single equation that proves nothing leaked between the cracks of the three-way match. When that equation fails to close by even a cent, an operator knows before any customer does that some money is unaccounted for, and the reconciliation is not declared complete until the residual is either explained or explicitly parked in suspense with an owner and a clock. Completeness is not a feeling; it is an arithmetic identity that either balances or does not.