Skip to main content
The PaymentSheet is Zennopay’s prebuilt, native payment UI, modeled on Stripe’s PaymentSheet. 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 — debit your ledger, render the activity row.

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 the payout is still processing, or when polling times out; the payment resolves asynchronously and unspent funds are released automatically).

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. Creating the intent and returning the session token is the only backend work. It is documented end to end, with a complete Node.js reference implementation, in Build your session endpoint.
Trust model. Your publishable key (pk_...) identifies your app and is safe to ship in a binary. Your secret key (sk_...) HMAC-signs server-to-server calls and never leaves your backend. The session token the SDK holds is minted by Zennopay, scoped to a single intent, short-lived, and opaque to you. There is no partner key pair to generate. Both keys come from the Zennopay Console → Developers tab.

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). VietQR today, PromptPay next.
  • 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 payouts get 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.

    Payouts are 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.
  • New corridors — when a corridor lights up (Thailand PromptPay is next), existing integrations get it with zero client work.
  • 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.