payment.succeeded webhook to fulfill the order.
In live mode, Direct-QR requires a Pro plan or higher. In sandbox mode, it is available on every plan. Fonepay sandbox QR payments can move real money, so the NPR 1,000 per-payment and NPR 5,000 monthly caps still apply, and every QR minted in the last 30 minutes counts at full value towards the monthly cap until it is paid or the 30 minutes pass, even if it was replaced or never scanned.
When to use this
Use Direct-QR for a custom app, point-of-sale terminal, or product page that must show the QR in place. For most websites, hosted checkout is simpler and supports eSewa, Khalti, and Fonepay. Direct-QR is Fonepay-only.How it works
SSE updates the customer page; the signed webhook confirms payment to your server.
POST /v1/qr/fonepay and sends the returned QR image and events_url to the page. The page opens an EventSource; PayBridgeNP sends qr.scanned and qr.paid there while independently delivering the signed webhook to your server.
Endpoint
POST /v1/qr/fonepay
Authenticated with a secret API key.
Request
Send
customer.phone when you have it. In live mode, it is the only way
the buyer can receive an SMS payment confirmation. In sandbox, the rendered
SMS is recorded in your logs but never sent to a real phone.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. It requires the payments:write scope and the same mode-aware plan access as QR creation.
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.
One live QR per session. A refresh only mints a new QR when the current one is gone or inside its last 20 seconds. If the current QR is still fresh, you get it back with its existing expires_at (same shape, no new QR). So call refresh on qr.expired or when expires_at is close, not on a fixed timer to “reset the countdown”. If minting is briefly unavailable you get 503 with code: "fonepay_busy"; retry after a moment.
Full example
Webhooks vs SSE
Use SSE for immediate browser feedback. Use the signed, retriedpayment.succeeded webhook to update your database and fulfill the order. If the customer closes the page, the webhook still arrives.
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. - Refresh the same session. When
qr.expiredfires orexpires_atis close, callPOST /v1/qr/{id}/refresh. Keep the sameid,events_url, webhook, and openEventSource. - 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.
- Refund successful Direct-QR payments from the PayBridgeNP dashboard. Fonepay refunds are not available through the create-refund API.
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.