ZPay
Engineering

Reconciling QR Ph at scale

Lara Mendoza1 min read

QR Ph settlement looks simple on paper. A customer scans, the funds move, the merchant sees a credit. In practice, the metadata that comes back from each issuing PSP varies enough that "did this transaction land?" is a non-trivial question — especially across six branches and three banks.

This post is about how we built reconciliation for that mess.

The shape of the problem

A single QR Ph transaction touches at least four systems:

  1. The acquiring PSP that hosts the dynamic QR
  2. The issuing wallet (GCash, Maya, BPI, etc.)
  3. The BSP-mandated InstaPay clearing rail
  4. The merchant's settlement bank

Each one names the transaction differently. Reference numbers are reused across rails. Timestamps drift by minutes. And the settlement file from the bank typically arrives the next business day — not the same day.

What we built

Three things, in order:

  • A canonical transaction ID computed at acceptance time, propagated through every webhook and report we generate
  • A matching engine that uses amount, timestamp window, and reference fingerprint to collapse multiple PSP rows into one settled event
  • A break-resolution UI for the 0.3% of transactions that don't auto-match, with the source rows side-by-side

The matching engine is the load-bearing piece. We tried fuzzy matching by amount alone — too many false positives on busy days. We tried strict reference-equality — too many false negatives. The fingerprint hash, scoped to a 90-second window, hit 99.7% on the pilot.

The result

For our pilot merchant (a six-branch coffee chain), what used to be a three-day close at month-end is now done before lunch on day one. The finance lead spends the rest of the day on actual finance work.

We'll write up the matching algorithm in more detail next month.


← All posts