Skip to main content
The PaymentSheet is Zennopay’s prebuilt, native QR payment UI. Your backend creates a payment intent and Zennopay returns a short-lived session token; your app hands the intent ID and token to the SDK in a single call; the SDK renders the entire pay experience — QR scan, amount + FX quote, slide-to-pay, and result — natively, in-process, and returns one typed PaymentResult. There is no browser tab, no redirect, and no URL scheme. The camera scanner, slide-to-pay physics, quote refresh, retries, and status polling are all inside the sheet.
Animated flow: scan a VietQR code, review the amount and USD quote, slide to pay, see the success screen, and return to the wallet with the balance debited.

One payment, end to end, in the sample wallet app — all rendered by the SDK.

The flow, screen by screen

These are real captures from the sample wallet app paying a VietQR merchant in the sandbox.
1

Your app: the user taps Scan & Pay

Partner wallet home screen with a Scan and Pay button.

The partner app owns everything up to this point — wallet, balance, entry point.

2

PaymentSheet: scan the merchant QR

The PaymentSheet scanner screen with camera viewport and paste fallback.

Native camera scanner with torch, gallery, and paste-QR fallbacks. The raw payload is validated server-side.

3

PaymentSheet: amount + FX quote

The amount screen showing 3,500,000 VND and the 140.00 USD wallet debit, with slide-to-pay.

₫3,500,000 to the merchant, $140.00 from the user's wallet. Quotes refresh silently on expiry.

4

PaymentSheet: slide to pay → result

The receipt: Payment successful with amount, merchant, masked account, transaction ID, and purpose.

Slide-to-pay fires the confirm exactly once; the sheet polls to a terminal state and shows a shareable receipt.

5

Your app: back in control

Partner wallet home after payment: balance debited and a new activity row.

The sheet dismisses and your onResult callback fires. Refresh the wallet and activity from your backend.

Handle results in your app

Use PaymentResult to update the screen, then refresh wallet and transaction state from your backend. Post accounting changes from verified server-side state. A client callback can be missed when the app closes and must not trigger a second wallet debit. pending means delivery is unresolved when the sheet closes. Keep tracking the existing intent through webhooks or a status read.

Pick your platform

Every platform has the same shape: one present call in, one PaymentResult out — completed / canceled / failed / pending (delivered when the user leaves while merchant delivery is still processing, or when polling times out; the payment resolves asynchronously). Keep it pending until your backend verifies the final payment state.

How it works

Per payment, your backend makes one HMAC-signed call — it creates the intent (passing the user’s KYC + sanctions attestations) — and Zennopay returns a short-lived session token bound to that intent. Your app passes the intent ID and token to the SDK; the SDK talks to the Zennopay API directly with the session token as Authorization: Bearer. Your backend creates checkout sessions, refreshes expired tokens, and reconciles wallet and payment state from signed webhooks or API reads. Start with the Node.js integration example in Build your session endpoint.
Trust model. Your HMAC key ID and signing secret stay on your backend. The SDK holds only the Zennopay-minted session token, which is scoped to one intent, short-lived, and opaque to your app. Register each app package or bundle identifier separately; see Authentication.

What you don’t build

The SDK owns the entire payment surface, so you never build:
  • A QR scanner — camera capture, torch, gallery import, and a paste-QR fallback (which also makes the flow testable on simulators).
  • EMVCo parsing — the raw payload is validated authoritatively server-side (CRC, tags, merchant extraction). See Corridors for scheme and execution availability.
  • FX quoting UI — the sheet shows the bound local amount and the exact USD wallet debit, and silently re-quotes when a quote expires.
  • A confirm surface — slide-to-pay fires the confirm exactly once, with an idempotency key persisted before the network call.
  • Status polling, retries, and recovery — including re-minting an expired session via your refreshSession hook and recovering the true terminal state after a process death mid-confirm. Slow delivery gets a processing state with honest copy, a “taking longer than usual” escalation, and a pending result if the user leaves early:
    The processing screen: spinner, 'this can take up to 30 seconds', a tip that the merchant may have already received the payment, and a Done button.

    Merchant delivery is asynchronous. The sheet says so instead of spinning silently, and the user can safely leave.

  • Per-user regulatory limits — Zennopay enforces corridor limits automatically (Vietnam: ₫5,000,000 per transaction, ₫10,000,000 per day, ₫25,000,000 per month per user) and the sheet renders the right copy.
  • Corridor parsing and binding — the server checks the QR scheme, country, and currency against the corridor bound into the session. Philippines/QR Ph is not exposed by the current v1 API; Thailand is an API/parsing preview and Vietnam is implemented subject to account enablement.
  • The receipt surface — you keep your own transaction-history list, but you never build the authoritative receipt view. When a user taps a past payment, presentReceipt reopens the branded Zennopay receipt with its live status (pending, captured, failed, or refunded).

Sample app

The screenshots above come from the Zennopay sample wallet app — a reference integration that exercises the session endpoint contract and the presentCheckout call against the sandbox. Ask your Zennopay integration engineer for the sample project.

Migrating from hosted checkout (beta)

The beta hosted-checkout model — a browser tab via ASWebAuthenticationSession / Chrome Custom Tabs with a URL-scheme redirect — is deprecated and removed. Zennopay.openCheckout(...) and the registered URL scheme are gone. Replace the call with presentCheckout / presentSheet, delete your URL-scheme registration, and add the camera usage declaration. See the migration notes at the top of the iOS and Android pages.

Next steps

iOS

Android

Flutter

React Native

Build your session endpoint

The one backend route every payment starts with.

Reopen a receipt

You keep your history list; the SDK reopens the authoritative receipt.

Test your integration

The sandbox loops to run before release.