Skip to main content
The Direct-QR API skips the PayBridgeNP hosted checkout entirely. You collect the customer’s details on your own page, call one endpoint, and get back a Fonepay QR string + image plus a real-time event stream you subscribe to from the browser. When the customer pays, you receive 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.
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. 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:
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.

Full example

Server side (Node/Bun example):

Webhooks vs SSE - use both

The SSE stream is for the customer’s browser: instant UI feedback. The payment.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 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.
  • For the best scan experience, refresh the QR. When qr.expired fires (or proactively every ~2 minutes), call POST /v1/qr/{id}/refresh to show a fresh, scannable QR for the same session - same id, events_url, and webhook, so keep your EventSource open. (Calling POST /v1/qr/fonepay again also works, but it spawns a new session per QR.)
  • 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.
This mirrors a common pattern: a short-lived QR for display, with durable confirmation on the backend.

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 (incompleteactive) 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).
  • 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.