The Six Steps of the Payment Reconciliation Process
Clone Formance Ledger
Formance Ledger is an open-source, double-entry ledger with those properties built in. Clone it on GitHub, run it locally, and post your first reconciliation transaction.
Clone Formance Ledger
Formance Ledger is an open-source, double-entry ledger with those properties built in. Clone it on GitHub, run it locally, and post your first reconciliation transaction.
In March 2023, Revolut filed its 2021 accounts months late because its auditor could not verify its reported revenue. The systems generating that revenue could not produce a per-transaction record anyone could reconcile.
A reconciliation failure like Revolut's can lock end users out of their funds, trigger enforcement, and freeze a banking partnership inside one settlement cycle. Financial operators at fintechs and platforms use transaction-level reconciliation to keep internal ledgers aligned with external payment rails, and discrepancies that go uninvestigated become customer, audit, and regulatory problems.
Reconciliation fails in two root categories. The first is data drift, which is when the internal ledger and the external payment rails record the same events differently in different cycles. The second is the unreconciliable ledger: the ledger's data model cannot produce a reliable position at all, so there is nothing trustworthy to compare against.
This article walks through the payment reconciliation process end to end, translating the failure modes above into a concrete six-step workflow, how often to run it, where it breaks in production, and how to build a robust payment reconciliation process.
Payment reconciliation at the ledger layer is a verification step that compares every ledger posting in the internal double-entry ledger against the corresponding settled record from the external payment rail. The control confirms that the ledger's position matches the rail's cleared position at a specific point in time.
Payment reconciliation answers one question: does the money the ledger says is held match the money the rails say is held, transaction by transaction? The goal is to track every cent.
It also operates at the transaction level across multiple payment rails at once, compared to bank reconciliation, which compares aggregate ledger balances to bank statements. The transaction-vs-balance distinction matters for regulators when the account structure involves an omnibus account. FDIC pass-through insurance requires that "the identity and ownership interest of each owner is ascertainable" from deposit account records, and an aggregate balance match cannot reconstruct per-owner positions. Balance-level reconciliation detects that a discrepancy exists, while transaction-level reconciliation localizes its source.
At Formance, we treat the internal ledger as the system of record, and matching runs directly against the postings it holds. The process runs as two parallel data pulls into a single matching layer, and every stage must be idempotent, which means reprocessing the same period twice produces the same output and no additional flags.
Reconciliation also surfaces discrepancies and does not correct them. Corrective postings happen through a deliberate, auditable adjustment posting under a separate workflow, with its own approval path.
The six steps of payment reconciliation are: collect rail data, extract ledger postings, match on a stable external identifier, flag and classify unmatched items, investigate root cause, and post adjustments to close the period.
Data collection pulls settlement files, bank statements, card network reports, and digital asset custodian exports from every active payment rail, then normalizes them into a single data model before matching begins.
Provider formats vary across flat files, bank statement exports, webhooks, and ISO 20022 XML messages. Handle this heterogeneity during normalization before matching. Otherwise, the matcher ends up relying on brittle approximations such as loose timestamps and manual review.
Keep the raw source file alongside the normalized record; investigators will need it during exception resolution.
Pull the internal postings from the core ledger for the matching date range, including pending, settled, and failed transaction states. A complete internal position covers every leg of every flow of funds, including interim states and final settled amounts.
Omitting pending postings guarantees false "missing transaction" flags for anything that settled after the ledger posted; omitting failed ones hides retries that should never have posted at all.
Match transactions on a stable external identifier such as a payment service provider (PSP) transaction ID or a bank reference number. For digital asset flows, the equivalent is an on-chain transaction hash.
Never match on amount alone because it is not unique across rails or time windows, and fee deductions can make the rail's figure and the ledger's figure differ on card transactions. In practice, reconciliation uses identifiers, amounts, dates, and reference fields together to match records.
The identifier only works if the ledger stores it at posting time. The example below is written in Numscript, Formance's purpose-built language for describing financial transactions. It records a payment-provider transaction of $20,000 and attaches the provider transfer identifier as the transaction's external reference.
The payment provider settlement boundary is @counterparties:paymentProviders:stripe:settlement, and the merchant payable account is @merchants:acme:payable:
// PROVIDER_SETTLEMENT
// Event: record a payment-provider transfer settled to a merchant
send [USD/2 2000000] (
source = @counterparties:paymentProviders:stripe:settlement allowing unbounded overdraft
destination = @merchants:acme:payable
)
set_tx_meta("event_type", "provider_settlement")
set_tx_meta("provider_transfer_id", "provider_transfer_123")
When posted, the transaction carries a provider-specific transfer identifier. The matcher binds the rail record to the ledger posting on provider_transfer_id deterministically. This means there are no fuzzy date windows, no amount tolerance bands, and no heuristics.
Flag every unmatched item at detection time and classify it against a small set of break categories that cover most production cases, with long-tail exceptions routed to separate workflows.
Common classifications include:
Classify at flag time, and capture the source posting ID, the external event ID, the discrepancy amount, and the classification in the exception record. A generic "unmatched" flag with only an amount attached creates a backlog.
Every flagged item needs a root-cause determination before any corrective posting is written, because treating a timing gap the same as a missing transaction produces a second error. Posting an adjustment for a transaction that settles on its own the next day leaves the books incorrect.
Duplicates and unclassified exceptions stall resolution because the investigator has to reconstruct context across the ledger, the PSP dashboard, and the raw settlement file.
Close the period by confirming balances and completing the close process according to the ledger and settlement rules in use. Corrections that get the books there use new reversal postings, so the record of the mistake and the fix both survive.
The confirmation itself must be a fixed, immutable close record. When an examiner asks what the position was on a given date, the immutable close record is the answer.
Reconciliation frequency should match the settlement window of the highest-risk rail in the stack. If a PSP settles daily, daily reconciliation is the minimum viable frequency.
Rails that settle in real time, including instant payment schemes and on-chain transactions, need continuous or near-real-time reconciliation to contain a discrepancy before it compounds across thousands of transactions.
The Financial Conduct Authority's (FCA's) PS25/12 safeguarding regime, effective 7 May 2026, requires payment firms to reconcile relevant funds at least once each reconciliation day.
The case for higher reconciliation frequency is also operational. Investigation cost scales with elapsed time. A discrepancy caught the same day is a matching problem. In contrast, a discrepancy caught 30 days later is an archaeology problem, with ledger context, PSP support tickets, and rail-side settlement files all harder to correlate.
Exception queues also grow non-linearly, because timing gaps, retries, and partial settlements that would have self-resolved within a day accumulate into a backlog when reconciliation runs weekly or monthly. Between reconciliation cycles, the ledger's stated position is unverified, and any treasury, working-capital, or safeguarding decision made from that position carries a widening confidence interval.
Reconciliation breaks in production at four points: timing gaps, partial settlements, retry storms and ghost postings, and multi-rail flows.
Timing gaps are the most common production failure mode, and multi-jurisdiction stacks make them worse.
Single Euro Payments Area (SEPA) credit transfers settle within one banking business day under the European Payments Council (EPC) rulebook.
A stack spanning SEPA, ACH, and on-chain rails produces legitimate transactions that look missing until each rail's window closes. The discrepancy classifier has to encode each rail's settlement convention, or every T+1 settlement becomes a false exception.
Partial settlements break simple amount-match logic because the rail delivers one number and the ledger expects another. Some settlement reports arrive net of fees or other charges. A naive matcher flags a residual on every such transaction, and the residual is not always an error. The reconciliation layer has to encode the fee structure and verify that gross minus fees equals net, rather than compare raw totals.
Retry storms generate ghost postings. A PSP webhook fires three times for one settlement event, the application layer processes all three without idempotency enforcement, and the ledger holds two extra postings with no rail counterparts.
Ghost postings never resolve on their own because they require a forced adjustment plus a fix to the ingestion path that created them.
Multi-rail flows multiply the reconciliation points per transaction. A single customer payment that touches a Banking-as-a-Service (BaaS) provider and a PSP in sequence has a settlement record at each hop, and each hop can drift independently.
The ledger must track every leg as its own posting rather than reconciling only the final settled amount. Otherwise, a discrepancy in the middle leg hides inside a total that happens to balance.
Four ledger invariants keep reconciliation tractable at any transaction volume: unique external-ID mapping, immutable postings, the double-entry constraint at every state, and bi-temporality. The Revolut audit failure maps directly to the first of these, and it is the canonical example of the unreconciliable ledger in production.
The failures that break in production share a root cause: they all become tractable when the ledger enforces a small set of structural invariants that keep it from becoming an unreconciliable ledger.
Bi-temporality maps directly to the FCA's safeguarding proposals that require firms to perform a reconciliation after insolvency following a failure, which is a point-in-time query against a historical window. A ledger that only stores current balances cannot answer that query.
Building a proper reconciliation process comes down to seven design decisions: anchor matching to the ledger, capture external identifiers at posting time, enforce idempotency at every stage, classify exceptions on detection, separate detection from correction, match cadence to the fastest rail, and close each period with an immutable record.
Treat the internal double-entry ledger as the single source of truth, and make every rail record reconcile against it. If reconciliation runs against a warehouse copy, a reporting database, or an aggregated balance table, drift will hide in the gap between the ledger and whatever the matcher actually reads.
Matching should execute directly against the same immutable postings the ledger holds, so the reconciled position and the accounting position are always the same number.
Every posting must carry the external identifier from the rail it represents: a PSP transaction ID, a bank reference, or an on-chain transaction hash. Attach the identifier when the posting is written, not later through a backfill. Without it, matching falls back to fuzzy heuristics on amount and date, and fuzzy matching is where silent errors accumulate.
Ingestion, matching, and adjustment must all be idempotent. Reprocessing the same settlement file, replaying the same webhook, or rerunning the same reconciliation window should produce the same result, not additional postings or duplicate exceptions. Idempotency is what makes retries safe and what keeps ghost postings out of the ledger in the first place.
Do not let unmatched items pile up in a generic queue. When a break is detected, classify it: timing gap, missing transaction, duplicate posting, amount mismatch, or fee-related residual. Capture the source posting ID, the external event ID, the discrepancy amount, and an assigned owner on the exception record itself. Classification at flag time is what keeps investigation cost from scaling with elapsed time.
Reconciliation surfaces discrepancies; it never silently corrects them. Corrections happen through explicit reversal postings under a separate, approval-gated workflow, and both the original mistake and the reversal remain in the ledger forever. This separation is what makes the process auditable so the examiner can always see what happened, when it was detected, and how it was resolved.
Run reconciliation at least as often as the highest-frequency rail settles. Daily for T+1 PSPs, continuous or near-real-time for instant payment schemes and on-chain flows. This ensures you meet regulatory floor requirements and catch same-day discrepancies.
End every cycle with a fixed, immutable close record that states the reconciled position on that date. A spreadsheet cell or an overwritable report is a note, not a control. When an auditor or regulator asks what the books said on a given day, the immutable close record is the answer.
A reconciliation process starts with the ledger. If the ledger cannot store external identifiers on every posting, enforce idempotency at ingestion, and preserve immutable history, no workflow built on top of it will hold up under audit or scale.