Skip to content

Payment reconciliation: how to match orders, fees, refunds and payouts to your bank account

A practical guide to payment reconciliation: match orders to payments, explain your balance with the ledger, tie payouts to bank deposits, and fix gaps.

Published 5 min read

Hand-drawn illustration of an order list, a ledger and a bank statement joined by matching lines with ticks, and a person walking towards them
On this page
  1. Why don't payouts match your sales?
  2. Step 1: reconcile orders against payments
  3. Step 2: reconcile your balance with balance transactions
  4. Step 3: reconcile payouts against bank deposits
  5. Export everything as CSV
  6. A daily payment reconciliation routine
  7. Common reconciliation mismatches and their causes
  8. Rehearse reconciliation in the sandbox
  9. Summary

Payment reconciliation is checking that three records agree: the orders in your system, the payments your provider says it took, and the money that actually reached your bank. They never match line for line — fees come off, refunds and chargebacks move money back, payouts bundle many payments into one transfer — so reconciliation is about explaining every difference, not expecting none.

This guide breaks payment reconciliation into three steps you can run daily, shows which KukoPay data answers each one, and lists the mismatches you will meet most often and what causes them.

Why don't payouts match your sales?

Because between a sale and a bank deposit, money moves several times:

  • Fees are taken when a payment is credited, so your balance grows by the net amount, not the order amount;
  • Refunds and chargebacks take money back out, sometimes after a freeze;
  • Payouts bundle many payments into one transfer and carry their own fee;
  • Timing differs: a payment is credited at once but only becomes withdrawable once it has settled upstream — see what is payment settlement;
  • Currencies are separate: USD, HKD and CNY are separate balances with separate payouts, and nothing is converted automatically.

Reconciliation works when you match each layer separately instead of comparing a month's sales with a bank statement.

Step 1: reconcile orders against payments

Question: did every order I think is paid actually get paid — and nothing else?

  • Match on order numbers, never on payment_id. An order can go through several payment attempts as a buyer switches methods, so its payment_id can change. Use your out_trade_no and KukoPay's trade_no.
  • Paid means status: "paid" (or partially_refunded / refunded later). pending, failed and expired orders took no money.
  • Pull the period's orders with GET /v1/orders filtered by creation time, or export them as CSV (below). Compare with the orders your system marked paid from payment.succeeded webhooks.

Anything paid on KukoPay but not in your system means a missed webhook: recover it from the events API — payment webhook not received walks through the causes.

Step 2: reconcile your balance with balance transactions

Question: can I explain every change in my balance?

Balance transactions record every movement as a signed row. Summing amount over a period gives exactly the change in your available balance, so the ledger explains the balance from the first row.

TypeWhat it isSign
payment_creditA payment, at the full order amount+
fee_debitThe fee on that payment−
refund_freeze / refund_debit / refund_unfreezeA refund accepted, confirmed, or refused and released− / − / +
dispute_freeze / dispute_debit / dispute_unfreeze / dispute_feeA chargeback opened, lost, won or withdrawn, and its fee− / − / + / −
payout_freeze / payout_success / payout_fee / payout_unfreezeA payout requested, completed, its fee, or rejected and released− / − / − / +
manual_adjustmentA correction by the platform (live only), with its reason in description±

Three details save hours:

  • A successful payment is two rows — payment_credit plus fee_debit; their difference is the net amount. Match the pair to the order through reference_id, which carries the order, refund, dispute or payout number.
  • balance_before / balance_after track the available balance. A freeze lowers available and raises frozen; a debit out of frozen leaves available unchanged, so before and after are equal on that row.
  • Fees are snapshotted. Each paid order carries fee_details (fee group, percentage, fixed fee) as applied at the time, so a later rate change never alters a past payment. How cross-border payment fees are calculated explains the formula.

Step 3: reconcile payouts against bank deposits

Question: does every transfer into my bank match a payout?

A payout shows in the ledger as payout_freeze when requested, then payout_success (the net transferred) and payout_fee (the flat $5.00) when it completes — all with the payout number as reference_id. The amount that arrives is the payout minus $5.00, so a $1,000.00 payout arrives as $995.00.

  • Match payouts, not payments, to the bank. One deposit is one payout, which may contain hundreds of payments.
  • processing is not paid. A live payout is approved first and transferred afterwards; it becomes success, with the bank reference recorded, only when the transfer has been made. Do not book it as received before then. The states are in payouts.

Export everything as CSV

Orders, refunds, disputes and balance transactions can be exported with format=csv:

curl -G "https://api.kukopay.com/v1/balance_transactions" \
  -H "X-Api-Key: $KUKOPAY_API_KEY" \
  -d format=csv \
  -d "created[gte]=2026-11-01T00:00:00Z" \
  -d "created[lte]=2026-11-30T23:59:59Z" \
  -o 2026-11-ledger.csv
  • The file has a UTF-8 BOM, so Excel opens it correctly, and amounts are already converted to two decimals, ready to sum.
  • An export returns at most 10,000 rows and is limited to 10 exports a minute on its own allowance — slice long periods into smaller created[gte] / created[lte] windows.
  • A text field that starts with =, +, - or @ gets a leading apostrophe so a spreadsheet does not run it as a formula. That is deliberate, not corrupted data.

For automated jobs, the JSON lists are cursor-paginated (limit, starting_after), so rows arriving while you page are never skipped. See lists, pagination and export.

A daily payment reconciliation routine

Pull yesterday's paid orders and payment events

Compare with the orders your system fulfilled. Recover any missing events; investigate any order you fulfilled that is not paid.

Pull yesterday's balance transactions per currency

Check that the sum of amount equals the change in available balance, and that every row's reference_id matches an order, refund, dispute or payout you know about.

Match new bank deposits to completed payouts

Each deposit should equal a payout_success row; book the payout_fee as a cost.

Review what is still open

Pending refunds, open disputes and payouts in processing are not mistakes — they are money in transit. Note them and check again tomorrow.

Common reconciliation mismatches and their causes

What you seeUsual cause
Paid on KukoPay, unpaid in your systemA webhook was not received — recover it from the events API
A buyer was charged after the order closedRare late payment: refunded in full automatically, with payment.duplicate_refunded; your balance is never touched
Balance lower than sales minus feesRefunds, open chargebacks or payouts frozen
Available balance is negativeAn open chargeback; payouts wait until later payments make it good — see the chargeback guide
Refund requested, balance not yet debitedThe refund is pending; it is frozen until confirmed — see partial refunds explained
Payout approved, nothing in the bankIt is processing; the transfer has not been made yet
Totals do not add up across currenciesEach currency is its own balance; reconcile them separately

Rehearse reconciliation in the sandbox

The sandbox ledger starts with an opening_balance row of 5,000.00 per currency, and Top up test funds adds a sandbox_top_up row — both are expected, not anomalies. A sandbox payout completes immediately with the same rows a live payout ends with. Run a few payments, a partial refund and a payout, export the ledger, and reconcile it before real money moves; test credit card numbers lists the scenarios.

Summary

  • Payment reconciliation matches three layers: orders to payments, the balance to the ledger, payouts to bank deposits;
  • match on trade_no / out_trade_no, never on payment_id, and recover missed payments from the events API;
  • balance transactions explain the balance row by row — a payment is payment_credit plus fee_debit;
  • a bank deposit is one payout: the payout minus $5.00, booked when it is success, not processing;
  • export with format=csv in windows of up to 10,000 rows, and reconcile each currency on its own.

Try it in the sandbox

Create a merchant account and request a test key. The sandbox needs no business verification.