SMARTPAY

QRPH vs InstaPay vs PESONet: A Technical Comparison for Developers (2026)

I lead backend engineering for a marketplace that sells into the Philippines, and the single most confusing conversation I had during our payments build was with a local bank's integration team. I asked which of QRPH, InstaPay or PESONet we should "integrate". The polite answer was that I was comparing a QR code standard with two clearing rails, and that a merchant does not integrate any of the three directly. That reframing fixed our architecture. This article is the explanation I wish I had on day one: what each of the three actually is, where a developer's code touches them, how collection and disbursement flows differ on each, and how to design status handling and reconciliation so you do not get burned by the differences.

Three names, two layers

QRPH (also written QR Ph) is the Philippine national QR code standard. It defines how a payment request is encoded into a QR code, following the EMVCo merchant-presented and person-to-person formats, so that any participating wallet or bank app can scan any participating merchant's code. It is an initiation layer: it tells the payer's app who to pay and how much. It does not move money by itself.

InstaPay and PESONet are the two retail clearing rails under the central bank's National Retail Payment System. InstaPay clears individual transfers in real time, around the clock, and is designed for lower-value, time-sensitive payments. PESONet clears transfers in batches during banking days across scheduled settlement cycles, and is designed for higher-value or non-urgent payments such as payroll, supplier invoices and bulk disbursements. When a QRPH payment crosses institutions, the money moves over InstaPay. So "QRPH vs InstaPay" is not an either-or; a QRPH merchant payment from a different bank or wallet is an InstaPay transaction wearing a QR code.

The practical consequence is that the comparison you actually need to make is between integration surfaces: QR-initiated collection, account-transfer collection, real-time disbursement, and batch disbursement. The deeper two-rail comparison is in InstaPay vs PESONet: the technical difference, and the interoperability story of QRPH is in QRPH interoperability explained; this article sits on top of both.

Where your code actually plugs in

As a merchant or platform, you never call InstaPay or PESONet. Only participating banks and licensed e-money issuers connect to the clearing switches. Your code talks to one of them, or to a licensed payment operator that sits in front of them and exposes a merchant API. In both cases the API surface looks roughly like this:

  • Collection via QRPH: you create a dynamic QR for an order, display it, and receive a webhook when a payer's app has paid it. Under the hood the transfer clears over InstaPay if the payer is at another institution, or internally if the payer is at the same institution.
  • Collection via account transfer: you are assigned a virtual account or reference-tagged account; payers transfer to it from their bank or wallet using InstaPay (real time) or PESONet (batch), and you receive a webhook when funds are credited. See virtual account numbers explained.
  • Disbursement: you submit a payout with a beneficiary and a rail choice. InstaPay for real-time, PESONet for batch. You receive status updates as the rail accepts, settles or returns the item. Batch patterns are covered in batch payout in the Philippines.

Once you see the three names as options inside these three flows, the technical comparison becomes concrete.

Side-by-side: what differs for a developer

DimensionQRPH (initiation)InstaPay (rail)PESONet (rail)
What it isNational QR encoding standardReal-time retail clearing railBatch electronic fund transfer rail
Timing modelPayer scans and confirms; clears over InstaPay across institutionsPer-transaction, near-instant, continuous operationCollected into batches, cleared in scheduled cycles on banking days
Typical useIn-store and online collection, invoicesSmall transfers, refunds, urgent payouts, QR settlementPayroll, supplier payments, large transfers
Value profileFollows the rail it clears onSubject to a per-transaction cap set by the regulatorSuited to higher values; limits are set by the sending institution
Status modelPending until paid or expiredAccepted, then final success or failure within the sessionAccepted, then settled or returned at a later cycle
Reconciliation keyYour order reference in the QR additional dataRail trace or reference number per itemBatch id plus per-item reference; return files
Failure patternQR expires unpaid; duplicate scansTimeouts with ambiguous state; immediate rejectsRejects surface after the cycle; account-name mismatches

Two rows deserve emphasis. The status model row is the one that breaks naive integrations: InstaPay gives you an answer in the same session, PESONet does not. The failure pattern row is why you cannot share one state machine across rails.

Collection: dynamic QRPH versus account transfer

For checkout, we use dynamic QRPH codes. Each order gets its own code with the amount and our order reference embedded, so a payment can be matched to an order without the payer typing anything. The request to a licensed gateway looks like this:

curl -X POST https://api.example-gateway.com/v1/qrph/dynamic \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-20261016-000123" \
  -d '{
    "reference": "order-20261016-000123",
    "amount": "350.00",
    "currency": "PHP",
    "expires_in_minutes": 15,
    "notify_url": "https://shop.example.com/webhooks/qrph"
  }'

The response carries the QR payload string and an image. Render the payload yourself if you control the screen; use the image for emails or printed invoices. The webhook that follows tells you the payment status, the amount actually received, the payer institution and the rail trace number. Three rules we apply:

  1. Treat the webhook as a signal and confirm by fetching the payment by reference before marking the order paid.
  2. Store the rail trace number alongside your order. It is the only identifier the payer's bank will recognise if a dispute comes in.
  3. Expire aggressively and never reuse a code. A stale dynamic code paid after you cancelled the order is a refund you did not want to process.

Account-transfer collection (a virtual account per customer or per invoice) is better when payers are businesses paying from online banking, because many corporate banking portals do not scan QR codes. It also lets a payer choose PESONet for larger invoices. The cost is that you must handle partial payments and overpayments, which QR largely avoids because the amount is embedded. The deeper developer flow is in Philippines payment collection and disbursement: a developer's guide.

Disbursement: choosing the rail per payout

On the payout side the rail choice is explicit, and it should be a per-item decision rather than a global setting. Our rule set: refunds and anything a customer is waiting on go InstaPay; payroll, supplier runs and anything above the real-time cap go PESONet; anything that fails validation on InstaPay is retried on PESONet only after a human confirms the beneficiary details. The request pattern:

// Pick the rail per payout, keep the request idempotent, and treat "accepted" as non-final
async function sendPayout(p) {
  const rail = p.urgent ? 'INSTAPAY' : 'PESONET';
  const r = await fetch(`${BASE}/v1/payouts`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': p.payoutId
    },
    body: JSON.stringify({
      reference: p.payoutId,
      rail,
      amount: p.amount,
      currency: 'PHP',
      beneficiary: { bank_code: p.bankCode, account_number: p.accountNo, name: p.name },
      notify_url: 'https://ops.example.com/webhooks/payout'
    })
  });
  const body = await r.json();
  // ACCEPTED is not SETTLED: persist and wait for the webhook, or poll
  await db.payouts.update(p.payoutId, { status: body.status, trace: body.trace_number });
  return body;
}

Idempotency keys matter more here than anywhere else in the stack, because a retried payout is real money leaving twice. The pattern is described in idempotency keys: preventing duplicate payments.

Status handling and reconciliation across the two rails

We run two state machines. For InstaPay items the states are created, accepted, succeeded, failed, and unknown. "Unknown" exists for the timeout case: the request left our system, no final answer came back, and we must query by trace before doing anything else. We never retry an unknown item blindly. For PESONet items the states are created, accepted, in-cycle, settled, and returned. An accepted PESONet item is not money delivered; it is money queued. Returned items come back with a reason code, typically an invalid or closed account or a name mismatch, and they come back after the cycle has run, which can be the next banking day.

Reconciliation follows from this. InstaPay items reconcile per item against the gateway's transaction export, keyed by trace number. PESONet items reconcile per batch: the batch id ties our file to the gateway's, and per-item references tie each line to a payout. Our daily job pulls both exports, matches, and raises anything unmatched after the expected settlement window. Account-transfer collections add a third stream: inbound credits matched by virtual account reference, with partial and over-payments flagged for operations.

Failure modes and how we designed around them

  • InstaPay timeout: the payer's bank or the switch is slow and your call times out. Design: mark unknown, query by trace, and only then decide. Never surface "failed" to a customer on a timeout.
  • PESONet return after the cycle: a payroll line bounces the next day. Design: keep the payout open until settled, notify operations on return, and require a corrected beneficiary before resubmission.
  • Expired QR paid late: a payer screenshots the code and pays after expiry. Design: your gateway should reject it; if funds still arrive, treat as an unmatched credit and refund via InstaPay.
  • Duplicate scan: two people in a group scan the same dynamic code. Design: the gateway should accept the first and reject the second; your webhook handler must still be idempotent on the payment id.
  • Name mismatch on disbursement: beneficiary name does not match the account. Design: where the gateway offers account-name verification before sending, use it; otherwise collect names exactly as they appear at the bank.
  • Rail outage or maintenance: one rail is degraded. Design: payouts flagged urgent fall back to the other rail only when the amount and timing allow, and collections keep working because QR codes do not depend on your payout rail.

Decision matrix: which surface for which job

Job to be doneUseWhy
Checkout in store or in appDynamic QRPHAmount and reference embedded, any wallet or bank can pay, instant confirmation
Invoice to a business payerVirtual account (InstaPay or PESONet inbound)Corporate portals transfer rather than scan; larger values fit PESONet
Customer refundInstaPay payoutCustomer is waiting; real-time confirmation
Payroll or supplier runPESONet batch payoutBatch files, higher values, predictable cycles
Marketplace seller settlementRail chosen per item by amount and urgencyMixed values; keep one payout API with a rail field
Recurring billStatic or dynamic QRPH on the billPayer initiates on their schedule; reference ties to the account

Licensing: who is allowed to put you on these rails

Because merchants reach the rails only through participants, the entity you integrate with determines whether your flows are compliant and whether your funds are protected. A bank or a licensed e-money issuer can collect and disburse on your behalf; a licensed operator of payment systems can run the merchant-facing platform in front of them. Anything else is an unlicensed intermediary holding your money. The onboarding path for QR collection specifically is in how to become a QRPH merchant, and the foreign-merchant version in QRPH collection services for international businesses.

SMARTPAY operates as a BSP-registered Operator of Payment Systems (OPS) and holds an Electronic Money Issuer (EMI) licence. Dynamic and static QRPH collection, InstaPay and PESONet disbursement are exposed through one merchant API with a single webhook format, and settlement is handled by the licensed entity. Licensing status can be checked against the central bank's published lists.

Frequently asked questions

Is a QRPH payment an InstaPay transaction?

When the payer and the merchant are at different institutions, yes: the QR code initiates it and InstaPay clears it. When both sides are at the same institution it is an internal transfer. Your gateway abstracts this, but the trace numbers you receive will reflect it.

Can I receive a QRPH payment over PESONet?

No. QRPH merchant payments clear in real time. If you need batch inbound transfers, use a virtual account that payers can transfer to over PESONet.

Which rail should refunds use?

InstaPay, unless the amount exceeds the real-time cap, in which case split the refund or use PESONet and tell the customer when to expect it.

Why did my PESONet payout show "accepted" and then fail a day later?

Accepted means the item entered a batch. The receiving bank validates it during the cycle, and invalid or mismatched accounts are returned afterwards. Keep the payout open until you see a settled status.

Do I need separate API integrations for the three?

Not if you use a licensed gateway: one API with a collection endpoint, a payout endpoint with a rail field, and one webhook format. Integrating directly with a bank usually means separate products for QR, real-time transfers and batch files.

How do I test InstaPay timeouts and PESONet returns before go-live?

Ask your gateway for sandbox triggers that simulate a timeout and a returned item. If the sandbox cannot simulate them, build the handling anyway and verify it in production with small, controlled transactions.

What identifier do I give a customer who says they paid?

The rail trace or reference number from the webhook, together with your order reference. Their bank can look up the trace; your order reference is meaningless to it.

Final take

Stop asking which of QRPH, InstaPay and PESONet to integrate. Ask which initiation surface fits each collection job and which rail fits each payout job, then integrate with a licensed participant that exposes all of them through one API. Keep two state machines, one per rail, store trace numbers everywhere, make every payout idempotent, and reconcile InstaPay per item and PESONet per batch. That design has survived every edge case we have hit so far. If you want to walk through your own collection and payout mix, reach out on Telegram at @LouisSMARTPAY or by email at [email protected].

相关推荐