Skip to main content
Use this workflow when your application takes payments through a PSP (Stripe, PayPal, or similar) and you need to prove which processor transactions actually reached the bank. It matters most when settlement timing, fees, refunds, or missing payouts make a spreadsheet comparison unreliable.

Get the grain right first

This is the step that prevents the most wasted work. A bank row is a posted cash movement. The PSP row it should be compared against is a payout or settlement export, not an individual charge. One bank settlement often represents dozens or hundreds of PSP charges netted together. Comparing it directly against those charges produces a wall of unmatched and amount_diff events that have nothing to do with a real reconciliation problem. If your PSP export is charge-level and your bank export is settlement-level, aggregate the charges upstream into one row per payout before Reconify sees them, or use a many_to_many pass with the payout ID as the group key so Reconify sums both sides for you. See Matching algorithm: split settlements for the mechanics and a worked example.

Configure both sides

The asymmetry here is deliberate and worth calling out loudly: the bank export stores major units (1,842.30), so multiplier: 100 converts it to 184230 minor units. The PSP export already stores minor units (184230), so its multiplier is 1. Get this backwards on either side and every amount is off by a factor of 100, and almost every row becomes an amount_diff. A few Stripe-style specifics that trip people up:
  • Timestamps in payout exports often need a full Go time layout, not just a date: a value like 2024-01-15T14:30:00Z needs date_layout: "2006-01-02T15:04:05Z07:00", not 2006-01-02.
  • These exports often carry several amount columns: gross, fee, net, and available balance. Picking the wrong one produces a wall of amount_diff events that look like a mapping bug but are actually a column choice. net_amount is usually the one that matches what actually lands in the bank.
  • Refunds and disputes legitimately reverse the sign of an amount. Don’t treat a negative PSP row as an error before checking whether it’s a genuine reversal.

Start strict

Begin with amount_tolerance_minor: 0, name_mode: none, and a date_window that reflects a known settlement delay, such as 2d. Widen the window when you have a specific, known delay to accommodate, never to make a bad mapping look like it’s working. A wide window hides the same mapping problems it’s supposed to route around.

Validate, then run

NDJSON is the practical choice for a scheduled job: downstream consumers process one event at a time instead of waiting on a single buffered JSON object. --audit records run provenance (file hashes, timestamps, the applied config) in the output, which matters for a monthly artefact someone might need to defend later. See Read the results for what run_info contains.

Triage the exceptions

Variant: bank statement vs internal ledger

The same workflow applies when the counterpart is your own ledger instead of a PSP export, with one addition: a pre-flight checklist, because ledger exports vary more than PSP exports do. Before configuring the pair, confirm:
  • Both sides use the same sign convention for credits and debits.
  • Reversals and refunds get the same treatment on both sides.
  • Pending ledger rows are excluded, or kept in a separate source from settled ones.
  • A stable payment, journal, or transfer identifier is available for ref_col.
Exception triage follows the same shape as the PSP case, with ledger-specific causes: an unmatched bank record is usually a fee, a manual transaction, a delayed import, or a posting your ledger hasn’t made yet. Confirm sign, currency, and rounding before touching amount_tolerance_minor, and compare the ledger event date against the bank value date before widening date_window.