Skip to main content
Direct-QR puts a Fonepay QR on your own page. One API call returns the QR image and a live browser event stream. Use the stream to update the page and the signed 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

Direct QR sequence from server creation through live browser events and the signed payment webhook

SSE updates the customer page; the signed webhook confirms payment to your server.

Your server calls 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.
Response (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:
The session’s overall 30-minute lifetime is not extended - only the ~3-min QR display window resets. Keep your 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

Server side (Node/Bun example):

Webhooks vs SSE

Use SSE for immediate browser feedback. Use the signed, retried payment.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 the payment.succeeded webhook still fire. Never mark the order failed just because qr.expired fired - wait for qr.paid, the webhook, or the session’s full lifetime.
  • Refresh the same session. When qr.expired fires or expires_at is close, call POST /v1/qr/{id}/refresh. Keep the same id, events_url, webhook, and open EventSource.
  • Settle on the webhook. Treat payment.succeeded as 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.
  • Hosted checkout - the standard multi-provider flow with no Pro gate.
  • Webhook verification - verify payment.succeeded deliveries on your server.
  • Sandbox testing - Fonepay has no test environment; sandbox Fonepay uses your real merchant credentials with small platform caps.