GH GambleHub

Reconciliation of PSP payments and reports

TL; DR

Reconciliation is the daily automated stitching of your ledger and events (auth/capture/refund/payout) with PSP/acquirer/bank reports. The key to success: a single data model, deterministic matching keys, strict idempotency, sum/FX/time tolerances, DLQ queue for controversial cases and auto-correction playbooks. KPI: Recon Mismatch Rate↓, Aging of Unreconciled↓, Auto-match%↑.

1) Why and what we check

Objectives: confirmation of revenue and commissions, duplicate/loss detection, correct settlement by days and currencies, refund-to-source control, compliance with audit/regulator.

Reconciliation objects:
  • Deposits: `auth → capture → settle`
  • Refunds: full/partial, statuses and amounts
  • Payouts/Withdrawals: Outgoing Method Payments
  • Fees & Adjustments: PSP fees, interchange schemes, corrections
  • Chargebacks/Disputes: Beyond Your Initiative
  • FX/Conversions: rates, spreads, fixing point

2) Data sources

Internal Events: event bus/Kafka, 'payments _ flat', 'refunds', 'payouts', your Ledger.

PSP Reports (SFTP/API/webhook dump):
  • Transactions (operational statements)
  • Settlements/Batches
  • Fees/Statements
  • Chargebacks/Disputes
  • Payouts/OCT/RTP/SEPA registries
  • Bank Statements: CAMT MT940/CSV/ISO20022, credit lift.
💡 Storage: landing → raw → normalized → matched. All incoming files - with checksums and versioning.

3) Matching keys

Priority key tree (descending in accuracy):

1. provider_txid ↔ provider_txid (unique PSP ID)

2. idempotency_key/ merchant_reference (if stable in PSP)

3. (amount, currency, timestamp_bucket, last4/bin, auth_code)

4. Fuzzy layer: ± tolerances by sum/time, BIN/issuer country, status family

Recommendations:
  • Keep both 'payment _ id' and 'provider _ txid'.
  • For partial/refund, add'sequence _ index'or'refund _ line _ id'.
  • Для payouts — `payout_batch_id + line_id`.
  • For FX - 'exec _ id' (conversion) and the source of the rate.

4) Data model (normalized layer)

json
{
"source": "INTERNAL    PSP_TX    PSP_SETTLEMENT    BANK",
"provider": "Acquirer_A",
"payment_id": "pay_123",
"provider_txid": "psp_tx_789",
"kind": "AUTH    CAPTURE    REFUND    PAYOUT    FEE    SETTLEMENT    CHARGEBACK",
"sequence": 0,
"amount": 100. 00,
"currency": "EUR",
"fee_amount": 1. 20,
"fx_rate": 1. 0000,
"fx_src": "PSP    ECB    BANK",
"status": "APPROVED    CAPTURED    SUCCESS    FAILED    SETTLED",
"event_ts": "2025-11-03T12:00:00Z",
"settlement_date": "2025-11-05",
"account": "PSP_MERCHANT_CARD_A",
"matching_keys": {
"provider_txid": "psp_tx_789",
"merchant_ref": "mr_456",
"idem_key": "idem_abc"
},
"hash_row": "sha256(...)"
}

5) Reconciliation process (ETL/orchestration)

1. Ingest: we take PSP reports (SFTP/API), validate the scheme/signatures, save to 'raw'.
2. Normalize: mappim of the field to the unified format (currency ISO, decimals, UTC time zone).
3. Match: algorithm for matching the key tree with tolerances.
4. Post-match: form diff (discrepancies) and journal entries for the ledger/corrections.
5. Settle: stitch 'PSP _ SETTLEMENT ↔ BANK' (crediting to the account), scatter by day/batch.
6. Report: dashboard, alerts; controversial in DLQ for manual parsing/auto-replaying.

Idempotence: for each file/page - 'ingest _ id'. Reloading does not change the result.

6) Tolerances and rules

Time: '± 15 min' for transactions, '± 1 day' for settlement.
Amount: '≤ 0. 01 'base currency or' ≤ 10 bps' for FX/fee differences.
FX: we allow discrepancy with the bank if the source of the exchange rate is different; fix 'fx _ src'.
Partial/Multiple: The sum of partial/refund lines must equal the internal balance.

7) Diff taxonomy

Diff typeDescriptionAction
MISSING_INTERNALThe PSP has, we don'tCreate orphan case, check webhooks/retrays
MISSING_PSPWe have, PSP has notCheck status/repeat, PSP contact
AMOUNT_MISMATCHAmounts vary> toleranceAutocorrect/Log, Escalate as needed
FEE_MISMATCHCommissions differAccept PSP as "true" (policy) or require credit note
STATUS_DRIFTCAPTURE with us, AUTH with PSPCheck capture/settlement webhooks
DUPLICATEDuplicate linesDedup by 'provider _ txid/idempotency _ key'
FX_DRIFTCourses divergeSet official source, adjust P&L
REFUND_OVERRefund > capturedUrgent unit, manual parsing, reverse correction log

8) Ledger & Accounting

Capture: `DR Accounts Receivable / CR Revenue` и `DR Cash (upon settle) / CR Accounts Receivable`

Fee: `DR Fees / CR Cash or Payable`

Refund: reverse postings pro rata partial

Chargeback: separate accounts and reserve for disputes

FX reval: daily revaluation of AR/cache balance at 'fx _ src _ policy'

9) KPIs and objectives

Auto-match% = auto-match lines/all lines (95% ≥ target)

Recon Mismatch Rate = diff lines/all lines (≤ 1-2%)

Aging of Unconciled: p50/p95 days in DLQ (p95 ≤ 3 days)

Settlement Time: proportion of batches stitched with D-day bank (≥ 99%)

Fee Accuracy: provider fee discrepancies (≤ 0. 1% of turnover)

Duplicate/Orphan Incidents: Aiming for 0

10) SQL slices

10. 1 Basic provider_txid matching

sql
WITH i AS (
SELECT provider, provider_txid, kind, amount, currency, event_ts
FROM internal_norm
),
p AS (
SELECT provider, provider_txid, kind, amount, currency, event_ts
FROM psp_norm
)
SELECT
COALESCE(i. provider_txid, p. provider_txid) AS txid,
COALESCE(i. provider, p. provider) AS provider,
i.kind AS kind_internal, p. kind AS kind_psp,
i.amount AS amount_internal, p. amount AS amount_psp,
i.currency, p. currency,
CASE
WHEN i.provider_txid IS NULL THEN 'MISSING_INTERNAL'
WHEN p. provider_txid IS NULL THEN 'MISSING_PSP'
WHEN ABS(i. amount - p. amount) > 0. 01 THEN 'AMOUNT_MISMATCH'
ELSE 'MATCHED'
END AS recon_status
FROM i
FULL OUTER JOIN p USING (provider, provider_txid);

10. 2 Settlement ↔ Bank

sql
SELECT s. settlement_date, s. batch_id, s. currency,
s. amount_settled, b. amount_bank,
(b. amount_bank - s. amount_settled) AS diff
FROM psp_settlements s
LEFT JOIN bank_statements b
ON b. value_date = s. settlement_date
AND b. currency = s. currency
AND ABS(b. amount_bank - s. amount_settled) <= 0. 5;

10. 3 Aging DLQ

sql
SELECT diff_type,
COUNT() AS cnt,
PERCENTILE_CONT(0. 5) WITHIN GROUP (ORDER BY AGE(NOW(), created_at)) AS p50_age,
PERCENTILE_CONT(0. 95) WITHIN GROUP (ORDER BY AGE(NOW(), created_at)) AS p95_age
FROM recon_dlq
GROUP BY diff_type
ORDER BY cnt DESC;

11) Features on rails/cases

Maps: differences between 'auth' and 'capture', late 'settlement' adjustments, interchange/circuit fee - separate lines.
A2A/Open Banking/RTP: instant confirmations, but 'reversal' possible; check 'payout' and returns.
Wallets: often perfect 'provider _ txid', quick 'refund'; watch the fee lines.
Vouchers: no symmetric refund - reflect correctly in policy and reports.
Crypto: on-chain hash ↔ provider_txid; confirmations N; accounting of network commissions and possible rebates; exchange rate - at the time of conversion.

12) Operational playbooks

Surge in MISSING_INTERNAL: check for loss of webhooks/retrays, replay ingestion, enable API polling.
AMOUNT_MISMATCH from one PSP: compare rounding/VAT/fee model, request a correction statement.

Settlement does not link to the bank: check value date, bank fees, T + N delays; temporarily put in the "Suspense Account."

Mass REFUND_OVER: immediate stop auto-refands, idempotency audit, manual correction.
FX_DRIFT: fix the policy of the exchange rate source (ECB/PSP/BANK), recalculate P&L differences.

13) Control and safety

Idempotence of ingestion: 'file _ id + checksum' and download history.
Access (RBAC) and 4-eye control: for manual corrections/journal entries.
Audit trail: all matches/diffs/corrections - in an unchangeable log.
Data quality: schemes, mandatory fields, currency/scale validation.

14) Dashboard and alerts

Widgets: Auto-match%, Mismatch Rate, Aging DLQ, Settlement Timing, Fee Accuracy, top PSP by diff, diff-type map.

Alerts:
  • 'Auto-match% <90% 'by provider/day → P1
  • 'Aging p95> 3 days' → P2
  • `AMOUNT_MISMATCH spike` → P1
  • 'Bank≠Settlement' by amount/currency → P0

15) Test Cases (UAT/Prod)

1. Reloading the same file → 0 side effects (idempotency).
2. Partial refands (3 lines) → amount matches, match by sequence.
3. FX conversion: exchange rate discrepancy within the tolerance → correct match.
4. Duplicate provider_txid in the report → dedup and alert.
5. The missing webhook capture → polling closed the gap, the status is aligned.
6. Settlement batch with fee line → correct breakdown on Revenue/Fee/Net.

16) Frequent mistakes and how to avoid

Compare 'attempt' vs' capture '→ keep the same granularity.
The absence of 'provider _ txid' in the → log loses the accuracy of the match.
Ignore timezone → offset by settlement dates.
There are no DLQ/retras → "eternal" discrepancies.
Manual edits without a log → inconsistency with auditing.

Fuzzy tolerances → either a re-match or "all in DLQ."

17) Implementation checklist

  • Unified normalization scheme and PSP/method/account directories
  • Mapping Key Tree (txid → merchant_ref → fuzzy)
  • Amount/Time Tolerances/FX, Course Source Policy
  • Idempotent ingestion, DLQ, retrai, alerts
  • Settlement↔Bank Reconciliation, Suspense Account Policy
  • Dashboard KPI, financial/audit reporting
  • Playbooks and SLA parsing diff cases

Summary

Reconciliation is an engineering discipline: normalization, reliable keys, tolerances, automatic matches and transparent corrections. With such a contour, you stabilize revenue and commissions, minimize "black holes," accelerate period closing and get audited without pain: Auto- match%↑, Mismatch↓, Aging↓.

Contact

Get in Touch

Reach out with any questions or support needs.We are always ready to help!

Telegram
@Gamble_GC
Start Integration

Email is required. Telegram or WhatsApp — optional.

Your Name optional
Email optional
Subject optional
Message optional
Telegram optional
@
If you include Telegram — we will reply there as well, in addition to Email.
WhatsApp optional
Format: +country code and number (e.g., +380XXXXXXXXX).

By clicking this button, you agree to data processing.