> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reconifyhq.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Reconify's API reference is read-only for customer data. Do not invent endpoints or authentication behavior beyond the OpenAPI contract.
> The public OpenAPI document contains only the documented external /v2 contract.

# Reconcile a bank statement against a PSP settlement

> Prove which processor transactions reached the bank, with the grain and multiplier mistakes handled before you configure anything.

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.

| Your comparison              | PSP export should represent                     | Avoid comparing directly                   |
| ---------------------------- | ----------------------------------------------- | ------------------------------------------ |
| Bank statement vs processor  | A payout or settlement                          | One bank payout against individual charges |
| Internal ledger vs processor | The ledger's recorded charge, refund, or payout | A net settlement against gross charges     |
| Charge-level investigation   | Individual balance transactions or charges      | Aggregated payout totals                   |

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](/cli/concepts/matching-algorithm#split-settlements-many_to_many) for the mechanics and a worked example.

## Configure both sides

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
sources:
  bank:
    file_pattern: "data/bank/*.csv"
    parser:
      type: csv
      date_col: "Date"
      date_layout: "2006-01-02"
      amount_col: "Amount"
      decimal: "."
      thousands: ","
      multiplier: 100
      currency_col: "Currency"
      ref_col: "Settlement reference"
      name_col: "Details"

  psp_settlements:
    file_pattern: "data/psp/*.csv"
    parser:
      type: csv
      date_col: "payout_date"
      date_layout: "2006-01-02"
      amount_col: "net_amount"
      multiplier: 1
      currency_col: "currency"
      ref_col: "payout_id"
      name_col: "description"

pairs:
  bank_vs_psp:
    left: bank
    right: psp_settlements
    date_window: "2d"
    amount_tolerance_minor: 0
    name_mode: "none"
```

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

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
reconify config validate --config reconify.yaml
reconify config check-source --config reconify.yaml --source bank --file data/bank/january.csv
reconify config check-source --config reconify.yaml --source psp_settlements --file data/psp/january.csv
reconify reconcile \
  --config reconify.yaml \
  --pair bank_vs_psp \
  --format ndjson \
  --audit \
  --out results/bank-vs-psp-2026-01.ndjson
```

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](/cli/guides/read-results#make-the-run-auditable) for what `run_info` contains.

## Triage the exceptions

| Outcome           | What it usually means                                    | First check                                                                             |
| ----------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `timing_diff`     | Settlement and bank posting dates differ.                | Compare the PSP payout date against the bank value date.                                |
| `amount_diff`     | A shared reference exists, but values disagree.          | Check whether one source is gross while the other is net of fees, refunds, or reserves. |
| `unmatched_left`  | A bank movement has no PSP settlement counterpart.       | Check for bank fees, chargebacks, manual transfers, or an incomplete PSP export.        |
| `unmatched_right` | A PSP settlement hasn't appeared in the bank source yet. | Check the settlement date, the bank statement period, and the payout status.            |

## 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`.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
pairs:
  bank_vs_ledger:
    left: bank
    right: ledger
    date_window: "2d"
    amount_tolerance_minor: 0
    name_mode: "none"
```

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`.
