qr.paid over Server-Sent Events at the same moment the payment.succeeded webhook fires on your server.
Direct-QR is a Pro feature. Below the Pro plan,
POST /v1/qr/fonepay returns 403 with entitlement: "fonepay.directQr".When to use this
Use Direct-QR when you want full control over the checkout UI - for example, a custom mobile app, a self-built point-of-sale terminal, or a marketplace where you want the QR embedded directly inside your product page rather than after a redirect. For most websites, the standard hosted checkout is simpler and gives you all three providers (eSewa, Khalti, Fonepay) in one flow. Direct-QR is Fonepay-only.How it works
1
Your server collects customer details
Name and email are required. Collect the phone too - it is the only way the buyer gets an SMS confirmation. Address is optional. You own the form.
2
Your server calls POST /v1/qr/fonepay
PayBridgeNP creates a checkout session, talks to Fonepay, and returns a QR string + a base64 PNG + a per-session event stream URL.
3
Your page shows the QR and subscribes to events
Use any QR renderer (or the bundled PNG) and open an
EventSource on events_url.4
Customer scans and pays
You receive
qr.scanned the moment the QR opens in their bank app, then qr.paid when the transfer completes - both pushed live, no polling.Endpoint
POST /v1/qr/fonepay
Authenticated with a secret API key.
Request
Always send
customer.phone when you have it. It is the only way the buyer
receives an SMS payment confirmation (Pro plans). The email receipt always
sends from customer.email, but in Nepal many buyers check SMS first - so a
missing phone means a quieter confirmation for them. Collect it on your form.201)
GET /v1/qr/:id/events
A Server-Sent Events stream. No API key required - the session ID in the URL is the auth token, so this is safe to call directly from a customer’s browser.
Replay on reconnect: if the connection drops and the client reconnects after a terminal event already fired, the stream replays it once with
replay: true set. So you can safely use the browser’s auto-reconnect without missing the payment.
POST /v1/qr/{id}/refresh
The Fonepay QR’s display window is only ~3 minutes, and some wallets (eSewa) reject a stale QR. Call this to mint a fresh QR for the same session - same id, same events_url, same webhook - instead of spawning a brand-new session per order. Requires the payments:write scope and a Pro plan.
It takes no body (the amount and customer already live on the session). The response is the same shape as create, with a new qr_message, qr_image, and expires_at:
EventSource open across refreshes: the session id is unchanged, so qr.scanned / qr.paid keep flowing on the same stream. A refresh returns 400 if the session is already paid, expired, or otherwise not refreshable.
Full example
Webhooks vs SSE - use both
The SSE stream is for the customer’s browser: instant UI feedback. Thepayment.succeeded webhook is for your server: durable, signed, retried. They fire from the same event, so use SSE to update the page and the webhook to commit your order to the database.
If the customer closes the browser between scan and confirmation, the SSE stream goes away - but the webhook still fires.
Expiry and retries
The QR has a ~3-minute display window (set by us, not Fonepay). When it lapses,qr.expired fires as a UI hint so you can show a fresh QR - but it is not the end of the payment:
- A late payment still completes. Some wallets (eSewa) reject a QR once the window passes, but others (Khalti) let the customer pay later. If they do, Fonepay confirms it - even a few minutes late - and both
qr.paid(SSE) and thepayment.succeededwebhook still fire. Never mark the order failed just becauseqr.expiredfired - wait forqr.paid, the webhook, or the session’s full lifetime. - For the best scan experience, refresh the QR. When
qr.expiredfires (or proactively every ~2 minutes), callPOST /v1/qr/{id}/refreshto show a fresh, scannable QR for the same session - sameid,events_url, and webhook, so keep yourEventSourceopen. (CallingPOST /v1/qr/fonepayagain also works, but it spawns a new session per QR.) - Settle on the webhook. Treat
payment.succeededas the source of truth for fulfilling the order - it fires whenever Fonepay confirms, fast or late. Use SSE only for live UI.
Pay a subscription invoice with Direct-QR
If you use subscription billing, you can collect a subscription’s invoice with a Direct-QR instead of sending the customer to the hosted bill page - perfect for in-person or embedded recurring collection (a counter membership, monthly tuition, etc.).POST /v1/billing/invoices/{id}/qr mints a Fonepay Direct-QR for that invoice (amount + customer come from the invoice - no body). When the customer scans and pays, the invoice is marked paid and the subscription activates (incomplete→active) automatically - the same outcome as paying via the bill page. The returned session is a normal Direct-QR session, so the events_url SSE stream and POST /v1/qr/{id}/refresh both work on it. Requires the billing:write scope (available on Growth and higher) and Fonepay credentials configured on the project.
Limitations
- Fonepay only. Other providers don’t have an equivalent QR-with-realtime-confirmation flow exposed today.
- NPR only.
- No partial captures or holds - the QR is a one-shot full-amount transfer.
- No refunds via this endpoint - refund a successful Direct-QR payment the same way as any other Fonepay payment (create-refund).
Related
- Hosted checkout - the standard multi-provider flow with no Pro gate.
- Webhook verification - verify
payment.succeededdeliveries on your server. - Sandbox testing - Fonepay has no test environment; sandbox Fonepay uses your real merchant credentials with small platform caps.