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:
| Approach | What the QR contains | Verdict |
|---|---|---|
| Self-contained | Amount + merchant + signature, all inside the QR | Works, 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 token | Recommended. 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.:
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: XPR-HMAC keyId=... , signature=... , ts=...
{
"amountMinor": 150000,
"currency": "VND",
"orderRef": "INV-2026-0714",
"description": "Table 5 — lunch",
"expiresInSec": 300
}
| Field | Type | Required | Description |
|---|---|---|---|
amountMinor | number | Yes | Amount in the currency's minor unit (e.g. đồng). |
currency | string | Yes | ISO currency code. |
orderRef | string | Yes | Vendor's own order/invoice reference (used for reconciliation). |
description | string | No | Shown to the user on the confirmation screen. |
expiresInSec | number | No | Intent TTL. Defaults to 300s; keep it short. |
{
"intentId": "PI_9f2a3c...",
"qrPayload": "https://pay.xperise/i/PI_9f2a3c...?s=...&v=1",
"status": "INITIATED",
"expiresAt": 1752480000000
}
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
| Threat | Mitigation |
|---|---|
| Replay (scan an old QR twice) | Intent is single-use; second pay on a CAPTURED/EXPIRED intent is rejected. |
| Screenshot reuse | Short TTL (≈5 min) + single-use. |
| Tampering with the amount | Amount 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 API | Vendor calls require API key + HMAC request signing over mTLS, with IP allowlisting. |
| Man-in-the-middle | TLS 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).