Skip to main content

QR-Code Payment

In xperise, QR payment is merchant-presented (MPM): the vendor generates a QR that represents a specific amount and order, and the employee scans it to pay. The vendor generates the QR with amount and transaction details; the employee scans to pay.

What a QR payment deducts​

QR payment follows the xperise two-layer model. On a successful payment, xperise deducts atomically:

  • Layer 1 — spending authority: the employee's monthly limit or cost center (whichever the company's operating model assigns).
  • Layer 2 — funding: the company wallet (backed by company credit).

If the allowance is exhausted or the wallet/credit is insufficient, the payment is declined and nothing is deducted.

Core recommendation: the QR carries a reference, not the money​

There are two ways to build a merchant-presented QR:

ApproachWhat the QR containsVerdict
Self-containedAmount + merchant + signature, all inside the QRWorks, but the QR is the instruction — larger payload, hard to revoke, replay window.
Reference to a server-side payment intent ✅An opaque, signed, single-use intent tokenRecommended. Small QR; the canonical amount/merchant is fetched from the server; the token is TTL-bound, single-use, and revocable.

With the recommended approach the vendor first creates a payment intent on the server, and the QR only encodes a signed pointer to it. The user's app resolves the intent from xperise (not from the QR) and shows the user the server's amount and merchant name — so a tampered or swapped QR cannot change what the user is actually asked to approve.

A resolvable QR payload is just a deep link, e.g.:

QR payload (single-use, short TTL)
https://pay.xperise/i/PI_9f2a... ?s=<hmac-sig>&v=1

Flow A — Vendor integrates the API​

The vendor's own backend/POS creates the intent server-to-server, renders the QR, and gets a webhook when the user pays.

Create a payment intent (illustrative)​

Authorization (vendor server-to-server)
Authorization: XPR-HMAC keyId=... , signature=... , ts=...
POST /v1/payments/intents
{
"amountMinor": 150000,
"currency": "VND",
"orderRef": "INV-2026-0714",
"description": "Table 5 — lunch",
"expiresInSec": 300
}
FieldTypeRequiredDescription
amountMinornumberYesAmount in the currency's minor unit (e.g. đồng).
currencystringYesISO currency code.
orderRefstringYesVendor's own order/invoice reference (used for reconciliation).
descriptionstringNoShown to the user on the confirmation screen.
expiresInSecnumberNoIntent TTL. Defaults to 300s; keep it short.
201 Created
{
"intentId": "PI_9f2a3c...",
"qrPayload": "https://pay.xperise/i/PI_9f2a3c...?s=...&v=1",
"status": "INITIATED",
"expiresAt": 1752480000000
}
Reconciliation

Always send your orderRef and an idempotency key on intent creation. If the request times out, retry with the same key — you get the same intent back instead of a duplicate charge.

Flow B — Vendor uses the xperise merchant app​

Vendors without engineering resources install the merchant app. They type the amount, the app calls the same POST /v1/payments/intents under the hood and displays the QR. No integration, same security. This is the recommended default for small vendors.

Handling the result​

The vendor learns the outcome through two independent channels, and should treat the server-to-server one as authoritative:

  • Webhook / server push (authoritative) — payment.completed, signed; the vendor verifies the signature and marks the order paid.
  • The user's success screen (convenience) — good UX, but never settle stock/goods on the user's screen alone; a screenshot can be faked.

QR-specific security​

ThreatMitigation
Replay (scan an old QR twice)Intent is single-use; second pay on a CAPTURED/EXPIRED intent is rejected.
Screenshot reuseShort TTL (≈5 min) + single-use.
Tampering with the amountAmount lives server-side; QR only points to it; the app shows the server's value. Payload is HMAC-signed.
QR-swap fraud (attacker replaces the vendor's printed QR)User confirmation screen shows the canonical merchant name from the server, not from the QR; risk engine flags mismatched merchant/geo. Dynamic QR on a screen is far safer than a static printed sticker.
Vendor impersonation via APIVendor calls require API key + HMAC request signing over mTLS, with IP allowlisting.
Man-in-the-middleTLS 1.2+; certificate pinning in the apps; signed webhooks.

See Security & Trade-offs for the full defense-in-depth model and the UX-vs-security decisions (e.g. when to require biometric confirmation).