All entries
Chapter VIII
Journal · 06 Oct 2026 · 11 min read

Payment Reconciliation: Stripe vs Adyen Reports

How to reconcile PSP payouts to your ledger and bank: Stripe balance transactions vs Adyen settlement batches, plus the breaks that cost you a week.

Your payment provider's dashboard says you took £84,210 last week. Your bank says £81,947 arrived. Both numbers are correct, and the £2,263 gap is a mix of fees, a dispute, a refund from the week before and one payout still in flight. Reconciliation is the job of proving that, line by line, before finance asks.

Most engineers meet it late, as a ticket titled "numbers don't match". The fix is rarely clever. It is knowing which identifier on the provider's side maps to which row on the bank statement, and which report to trust for which question. This guide covers how reconciliation works against Stripe, Adyen and Checkout.com, where it breaks, and how I would structure it.

What Is Payment Reconciliation and Why Does It Break?

Reconciliation means matching three records of the same money:

1. Your internal ledger: what your system believes happened (orders, refunds, payouts owed). 2. The provider's report: what the PSP says it processed, charged in fees and paid out. 3. The bank statement: what actually landed in your account.

Two-way matching (ledger to PSP) tells you the provider agrees with you. It does not tell you the money arrived. Three-way matching is the only version that survives an audit, and for anyone holding client money it is not optional. I covered the regulatory side for UK firms in the CASS 15 reconciliation guide; this piece is the PSP-facing counterpart.

The mismatches fall into a small number of buckets:

BreakWhy it happensWhere it shows up
TimingFunds are "available" days before a payout, and payout arrival differs from bank posting datePayout total ≠ bank credit on the day
Fees nettedProvider deducts fees before paying outGross sales ≠ payout
Refunds and disputesDebited from a later payout, not the originalNegative lines in an unrelated payout
FXConversion at the provider's rate, with a separate feeAmount in settlement currency differs from sum of charges
ReservesRolling reserve held back and later releasedPayout smaller, then larger
Failed payoutsBank rejected the credit, funds return to balancePayout in report, nothing in bank
RoundingSumming decimals instead of integersPennies, always

How Does Stripe Payout Reconciliation Work?

Stripe's model is the balance transaction. Every movement of money on your Stripe balance is one balance_transaction object, and a payout is just one more balance transaction that sweeps a set of others.

The fields that matter:

  • amount: gross, in minor units (pence, cents). Positive when you are owed, negative when money leaves.
  • fee and fee_details[]: fees, each tagged stripe_fee, application_fee, payment_method_passthrough_fee, tax or withheld_tax.
  • net: amount minus fee. This is what moves your balance.
  • available_on: the date the net funds become available. It is not the date the money reaches your bank.
  • exchange_rate: present when the charge currency differs from the settlement currency.
  • source: the ID of the object behind it (ch_, re_, a dispute).
  • type and reporting_category.
Use reporting_category for accounting, not type. Stripe's own guidance says so, and the reason is practical. type is a long, open-ended enum (it includes charge, payment, refund, payment_refund, payment_failure_refund, adjustment, payout, payout_failure, reserve_hold, reserve_release, payment_unreconciled and more), and Stripe adds values. Treat it as open. reporting_category collapses card and local-method variants, so charge and payment both become charge, and refund and payment_refund both become refund. Disputes arrive as adjustment types with the dispute as the source.

To list everything inside a payout:

GET /v1/balance_transactions?payout=po_1Nxxxxxxxx&expand[]=data.source&limit=100

Three catches I have seen bite people:

  • Default page size is 10. A payout with 400 transactions silently reconciles 10 of them if you forget pagination.
  • Only automatic payouts are itemised. Manual and instant payouts have no payout linkage, so Stripe cannot tell you what is inside. You reconcile those against the balance history yourself.
  • Retrieving the payout object does not return its transactions. You need the list call above.
Stripe also fires payout.reconciliation_completed when the itemised data is ready, and that event runs on a twice-daily cycle (data for 00:00 and 12:00 UTC). A payout can land in the bank before its reconciliation data exists. If your job runs on payout creation rather than on that event, you will intermittently reconcile an empty payout.

For bulk work, the Payout Reconciliation Report (payout_reconciliation.itemized.7 through the Reporting API) gives you automatic_payout_id, balance_transaction_id, charge_id, reporting_category, gross, fee, net, available_on, payout_reference_token and trace_id. Note the unit change: reports are in major units (12.34), the API in minor units (1234). That is a classic source of 100x errors, and the reason to keep money as integers everywhere and convert only at the edge. The same discipline is covered in the ISO 4217 minor units guide.

How Does Adyen Settlement Reconciliation Work?

Adyen organises around batches rather than a running balance. Money is grouped into a numbered settlement batch, the batch closes, and a MerchantPayout is generated. The Settlement details report is one row per transaction event, with a Batch Number column tying each row to its batch.

The default columns include Company Account, Merchant Account, Psp Reference, Merchant Reference, Payment Method, Creation Date, Type, Modification Reference, Gross Currency, Gross Debit (GC), Gross Credit (GC), Exchange Rate, Net Currency, Net Debit (NC), Net Credit (NC), Commission (NC), Markup (NC), Scheme Fees (NC), Interchange (NC) and Batch Number. The fee split is the useful part: Commission is Adyen's fee, Markup is the acquiring bank's, Scheme Fees go to Visa or Mastercard and Interchange to the issuer. Where interchange-level data is available, Commission is empty.

The Type column is where the logic lives:

TypeDirectionMeaning
SettledCreditPayment sent for settlement
SettledReversedDebitAdyen did not receive the funds within 30 days of capture
RefundedDebitRefund
RefundedReversedCreditRefund could not be credited to the shopper
ChargebackDebitDispute lost or undefended
ChargebackReversedCreditSuccessful defence, or shopper re-paid
SecondChargebackDebitFirst chargeback defence failed
FeeDebitAdyen transaction fees
MerchantPayoutDebitThe payout to your bank
BalanceTransferEitherCarries a balance into a later batch
ReserveAdjustmentEitherReserve movement

Two rows deserve a second look. BalanceTransfer appears when a batch cannot be paid out, for example because the net is negative after refunds, and it rolls the shortfall into the next batch. If you only match MerchantPayout rows to bank credits you will miss the batch that paid nothing. And SettledReversed means a payment you counted as revenue is clawed back weeks later. Revenue recognised at Settled needs a reversal path.

The aggregate Settlement details batch report gives you Batch Number, Batch Closed Date and transaction counts, which is the level you match to the bank statement. The Payment Accounting Report is the fee-focused companion: it carries 50-plus record types and fee columns in settlement or fee currency, built for invoice reconciliation rather than cash matching.

Stripe vs Adyen vs Checkout.com: Which Reconciles Best?

The three providers disagree on the unit of reconciliation, and that decides how much code you write.

StripeAdyenCheckout.com
UnitBalance transactionSettlement batchPayout ID
Link to payoutpayout filter on balance transactionsBatch Number column12-character payout ID in Financial Actions by Payout ID
Fee detailfee_details[] per lineSeparate Commission, Markup, Scheme, Interchange columnsFee categories: interchange, scheme, gateway, authentication and more
Real-time APIYes, plus webhookReports mainly, scheduledReports API
GotchaManual payouts are not itemisedBalanceTransfer batches pay nothingPayouts Report misaligns with invoicing periods

My view: Stripe is the easiest to automate because everything is an API object with a stable ID and a webhook that says "ready". Adyen is the most transparent about fees, since interchange and scheme fees are separate columns, which matters if you are checking whether you really are on interchange-plus pricing. Checkout.com sits between them. Its docs tell you to avoid the Payouts Report for invoicing and use monthly Balance or Financial Actions reports in UTC instead, which tells you what the timing problem looks like in practice.

If you run more than one PSP, normalise to your own schema at ingestion. Do not let each provider's vocabulary leak into your ledger.

How Do You Match a PSP Payout to a Bank Statement Line?

This is the step the provider docs underplay. You have a payout of £12,418.37 on the provider side and a credit on the bank feed. Matching on amount alone works until two payouts have the same amount, or one is split.

What you can use:

  • Stripe exposes a payout_reference_token ("displays on the beneficiary's bank statement") and, via trace_id, a bank-assigned identifier fetched up to 10 days after the payout is paid. The trace ID is unavailable for some countries and for UK Instant Payouts. statement_descriptor is displayed inconsistently by banks, so do not make it your primary key.
  • Checkout.com has the 12-character payout ID, which you match against the bank line and the "Payout Amount" field.
  • Adyen ties the MerchantPayout row and its Batch Closed Date to the credit. I could not find a documented reference format that Adyen writes to the bank statement, so test this on your own account before relying on it.
On the bank side, the structured format is ISO 20022 camt.053, where each Ntry can wrap a batch and the detail sits in TxDtls, with EndToEndId (up to 35 characters) and AcctSvcrRef as reference fields. Older feeds use MT940 or BAI2, which cram the detail into free text. If you are choosing a bank feed, camt.053 is the one that keeps references intact. The message families are laid out in the ISO 20022 message types guide.

One UK trap. ClearBank documents that its Faster Payments reference field accepts 35 characters but anything over 18 is truncated before it reaches the scheme. A long reference you thought was a unique key arrives cut off. Design matching to survive a truncated reference by combining amount, date window and a short token.

A workable algorithm, in order:

1. Exact match on provider reference or trace ID. 2. Amount plus date window (value date within the provider's stated arrival window ± 2 business days). 3. Group match: one bank credit equals several payouts, or the reverse. 4. Anything left goes to a human queue with the candidate matches attached.

What Reconciliation Breaks Should I Expect in 2026?

Timing is the largest source of false alarms. Stripe's default UK settlement runs 3 business days after an initial 7 calendar days for new accounts; US card payments are typically 2 business days; ACH debit 4; SEPA Direct Debit 6. A charge created on a Saturday starts its business-day clock on Monday. Build the expected-arrival calculation per payment method, then alert on payouts that are late against it, not on every unmatched row. Fees netted at source distort revenue. Reconcile on gross first, then fees as their own ledger entries. If you book net, you lose the ability to audit your effective rate. Disputes land in the wrong week. A chargeback debit is taken from a later payout. The line in that payout references an old charge. Your reconciler must follow source (Stripe) or the PSP reference (Adyen) back to the original payment, not treat the line as new activity. Partial captures hide a second entry. On Stripe, a partial capture produces a charge for the full authorisation plus a refund-type entry for the uncaptured portion, categorised as partial_capture_reversal. Count only the charge and your totals are off by exactly the uncaptured amount. The expiry rules behind this are in the card authorisation expiry guide. Stale unreconciled funds. Stripe has a payment_unreconciled type for customer funds that stayed unreconciled for more than 90 days. If you ever see it, a customer paid and nothing matched. Rounding. Sum integers. Convert once, at display.

What I Would Build: Opinion

I would not build reconciliation as a nightly script that diffs CSVs. I would build it as an append-only ledger of provider events, each with a provider ID and bank-match status, and treat "unmatched" as a first-class state with an owner and an age. A double-entry ledger gives you the right shape: a clearing account per provider, credited on Settled or charge, debited when the payout is matched to a bank line. The balance of the clearing account is your reconciliation status. If it is not zero (allowing for in-flight payouts), something is unmatched, and the account tells you how much.

That is an opinion, but a defensible one. A diff tells you today's numbers disagree. A clearing account tells you when the disagreement started and which event opened it.

What This Means for Engineering Teams

  • Pick the matching key per provider before writing code. Stripe payout ID plus payout_reference_token; Adyen batch number; Checkout.com payout ID. Store the bank-side reference next to it.
  • Trigger on readiness events, not creation. Use Stripe's payout.reconciliation_completed, and schedule Adyen report pulls after batch close.
  • Paginate everything. Set limit=100 and follow has_more. Test with a payout containing more than 10 items.
  • Store gross, fee and net separately, in integers. Convert from report major units at ingestion.
  • Model reversals explicitly. SettledReversed, RefundedReversed, payout_failure and ChargebackReversed are real states, not edge cases.
  • Run matching with a tolerance window and a human queue. Expected-late is not the same as missing.
  • For UK firms holding client money, tie the clearing account to your daily safeguarding check rather than keeping two processes.
I'm Tom Wang, and I've had to explain a three-pound reconciliation gap to an auditor. The gap was a rounding bug, and it took two days to prove. Build the matching trail first and the explanation takes ten minutes.