Displaying the QR
qr.payload is the string your QR code has to encode. Render it, and any bank or
wallet app in Nepal can scan it. Treat it as opaque: do not parse it, do not build one yourself,
and do not reuse one.
Rendering it
Do not send the payload to a third-party QR image service — it identifies your merchant account and the amount. Render it locally.
import QRCode from 'qrcode';
await QRCode.toCanvas(document.getElementById('qr'), payment.qr.payload, {
width: 240,
margin: 2,
errorCorrectionLevel: 'M',
});
class="tok-key">use BaconQrCode\Renderer\ImageRenderer;
class="tok-key">use BaconQrCode\Renderer\Image\SvgImageBackEnd;
class="tok-key">use BaconQrCode\Renderer\RendererStyle\RendererStyle;
class="tok-key">use BaconQrCode\Writer;
$writer = class="tok-key">new Writer(class="tok-key">new ImageRenderer(class="tok-key">new RendererStyle(class="tok-num">240), class="tok-key">new SvgImageBackEnd()));
class="tok-key">echo $writer->writeString($payment['qr']['payload']);
import qrcode
img = qrcode.make(payment["qr"]["payload"], error_correction=qrcode.ERROR_CORRECT_M)
img.save("payment.png")
Practical rules for a scannable code
- Error correction M. Payment payloads are long; level H pushes the module count high enough that phone cameras struggle.
- At least 200×200 px on screen, or 2.5 cm printed.
- Keep the quiet zone. Two modules of clear space on every side, or scanners fail to lock on.
- Dark on light, high contrast. Do not invert for dark mode — many scanners will not read a light-on-dark code. Keep the code itself on a white tile.
- Never put a logo over it. The extra data density leaves no redundancy to spare.
Show the amount next to it
A dynamic QR already carries the amount, so the customer cannot underpay. Displaying the figure beside the code anyway is what stops the far more common problem: someone paying the wrong order's QR.
Expiry
A QR is payable for five minutes. After qr.expires_at, create a new payment rather
than re-displaying the old payload.
When a payment reaches a terminal state, stop showing the QR. Cover or replace it. A stale code left on a counter screen is the easiest way for a customer to pay an order that was already settled or abandoned.
Polling for status
Webhooks are the right answer. Where you genuinely cannot receive them, poll:
curl https://qora.laayo.com.np/v1/payments/pay_7Fh2kQ9mXwRt3vNbLp6sDc/qr \
-H "Authorization: Bearer sk_test_YOUR_KEY"
{
"id": "pay_7Fh2kQ9mXwRt3vNbLp6sDc",
"status": "requires_payment",
"amount": 25000,
"qr": {
"payload": "00020101021230590016A0055...",
"expires_at": "2026-09-07T09:20:00+05:45",
"expired": false
}
}
Poll every two seconds while the QR is fresh, then back off. Do not poll faster: it will not make confirmation arrive sooner, and it will hit the rate limit.