> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zennopay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Zennopay PaymentSheet

> A native, in-process pay experience — scan → amount → slide-to-pay → result — your users never leave your app.

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.

<Frame caption="One payment, end to end, in the sample wallet app — all rendered by the SDK.">
  <img src="https://mintcdn.com/zennopay/tayCe7TFWcX18Mj6/images/sdk-demo/flow.gif?s=c8699af33ee49f473e5498535d9fd758" width="320" alt="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." data-path="images/sdk-demo/flow.gif" />
</Frame>

## The flow, screen by screen

These are real captures from the [sample wallet app](#sample-app) paying a VietQR
merchant in the sandbox.

<Steps>
  <Step title="Your app: the user taps Scan & Pay">
    <Frame caption="The partner app owns everything up to this point — wallet, balance, entry point.">
      <img src="https://mintcdn.com/zennopay/tayCe7TFWcX18Mj6/images/sdk-demo/01-home.png?fit=max&auto=format&n=tayCe7TFWcX18Mj6&q=85&s=ee517e44ae5e86e183b35ee297d391c3" width="280" alt="Partner wallet home screen with a Scan and Pay button." data-path="images/sdk-demo/01-home.png" />
    </Frame>
  </Step>

  <Step title="PaymentSheet: scan the merchant QR">
    <Frame caption="Native camera scanner with torch, gallery, and paste-QR fallbacks. The raw payload is validated server-side.">
      <img src="https://mintcdn.com/zennopay/tayCe7TFWcX18Mj6/images/sdk-demo/02-sheet-scan.png?fit=max&auto=format&n=tayCe7TFWcX18Mj6&q=85&s=19bbeeb16dbf033657064babb3ce86fa" width="280" alt="The PaymentSheet scanner screen with camera viewport and paste fallback." data-path="images/sdk-demo/02-sheet-scan.png" />
    </Frame>
  </Step>

  <Step title="PaymentSheet: amount + FX quote">
    <Frame caption="₫3,500,000 to the merchant, $140.00 from the user's wallet. Quotes refresh silently on expiry.">
      <img src="https://mintcdn.com/zennopay/tayCe7TFWcX18Mj6/images/sdk-demo/03-sheet-amount.png?fit=max&auto=format&n=tayCe7TFWcX18Mj6&q=85&s=a3ca02575acfd0723bba80bdfa725235" width="280" alt="The amount screen showing 3,500,000 VND and the 140.00 USD wallet debit, with slide-to-pay." data-path="images/sdk-demo/03-sheet-amount.png" />
    </Frame>
  </Step>

  <Step title="PaymentSheet: slide to pay → result">
    <Frame caption="Slide-to-pay fires the confirm exactly once; the sheet polls to a terminal state and shows a shareable receipt.">
      <img src="https://mintcdn.com/zennopay/tayCe7TFWcX18Mj6/images/sdk-demo/04-sheet-success.png?fit=max&auto=format&n=tayCe7TFWcX18Mj6&q=85&s=603e1cf35dff9cfd22c6f73d330e8a86" width="280" alt="The receipt: Payment successful with amount, merchant, masked account, transaction ID, and purpose." data-path="images/sdk-demo/04-sheet-success.png" />
    </Frame>
  </Step>

  <Step title="Your app: back in control">
    <Frame caption="The sheet dismisses and your onResult callback fires — debit your ledger, render the activity row.">
      <img src="https://mintcdn.com/zennopay/tayCe7TFWcX18Mj6/images/sdk-demo/05-home-after.png?fit=max&auto=format&n=tayCe7TFWcX18Mj6&q=85&s=107edf4f1251cd4808fad01554e51280" width="280" alt="Partner wallet home after payment: balance debited and a new activity row." data-path="images/sdk-demo/05-home-after.png" />
    </Frame>
  </Step>
</Steps>

## Pick your platform

| Platform                               | Package                           | Install                                   | Minimums                          |
| -------------------------------------- | --------------------------------- | ----------------------------------------- | --------------------------------- |
| [iOS](/payments/ios)                   | `Zennopay` (SwiftPM / CocoaPods)  | SPM `zennopay-ios-sdk` · `pod 'Zennopay'` | iOS 16, Swift 5.9                 |
| [Android](/payments/android)           | `in.zennopay:sdk` (Maven Central) | `implementation("in.zennopay:sdk:0.6.0")` | minSdk 24, Compose                |
| [Flutter](/payments/flutter)           | `zennopay_flutter` (pub.dev)      | `zennopay_flutter: ^0.6.0`                | Flutter 3.19, Dart 3.4            |
| [React Native](/payments/react-native) | `@zennopay/react-native` (npm)    | `npm install @zennopay/react-native`      | RN 0.70 (bridges the native SDKs) |

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`.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant U as User
    participant App as Partner app<br/>(+ PaymentSheet SDK)
    participant BE as Partner backend
    participant ZP as Zennopay API
    participant Rail as Local payout rail<br/>(Vietnam)

    U->>App: Tap "Scan & Pay"
    App->>BE: POST /wallet/checkout-session
    BE->>ZP: POST /v1/payment_intents (HMAC-signed + attestations)
    ZP-->>BE: intent_id + session_token (short-lived, single-use)
    BE-->>App: intent_id + session token
    App->>App: Zennopay.presentCheckout(intentId, sessionToken)
    U->>App: Scan merchant QR
    App->>ZP: POST /v1/payment_intents/:id/scan (Bearer)
    ZP-->>App: merchant + quote-bound amount (VND + USD)
    U->>App: Slide to pay
    App->>ZP: POST /v1/payment_intents/:id/confirm (Bearer, Idempotency-Key)
    ZP->>Rail: Payout to merchant's bank
    App->>ZP: GET /v1/payment_intents/:id (poll to terminal)
    ZP-->>App: status: captured
    App-->>U: "Payment sent" → sheet dismisses
    App->>App: onResult(.completed(intentId))
    ZP-->>BE: webhook: payment_intent.captured
```

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](/payments/session-endpoint).

<Info>
  **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.
</Info>

## 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:

  <Frame caption="Payouts are asynchronous. The sheet says so instead of spinning silently, and the user can safely leave.">
    <img src="https://mintcdn.com/zennopay/tayCe7TFWcX18Mj6/images/sdk-demo/06-sheet-processing.png?fit=max&auto=format&n=tayCe7TFWcX18Mj6&q=85&s=1c5725c445f50b07d217ec8e40b80acc" width="280" alt="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." data-path="images/sdk-demo/06-sheet-processing.png" />
  </Frame>
* **Per-user regulatory limits** — Zennopay enforces
  [corridor limits](/fundamentals/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`](/payments/reopen-receipt) 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](/payments/session-endpoint)
and the `presentCheckout` call against the sandbox. Ask your Zennopay
integration engineer for the sample project.

## Migrating from hosted checkout (beta)

<Warning>
  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](/payments/ios) and [Android](/payments/android) pages.
</Warning>

## Next steps

<CardGroup cols={2}>
  <Card title="iOS" icon="apple" href="/payments/ios" />

  <Card title="Android" icon="android" href="/payments/android" />

  <Card title="Flutter" icon="mobile" href="/payments/flutter" />

  <Card title="React Native" icon="react" href="/payments/react-native" />

  <Card title="Build your session endpoint" icon="server" href="/payments/session-endpoint">
    The one backend route every payment starts with.
  </Card>

  <Card title="Reopen a receipt" icon="receipt" href="/payments/reopen-receipt">
    You keep your history list; the SDK reopens the authoritative receipt.
  </Card>

  <Card title="Test your integration" icon="vial" href="/payments/testing">
    The sandbox loops to run before release.
  </Card>
</CardGroup>
