Skip to main content
Every PaymentSheet presentation starts with one round-trip to your backend. Your app asks for a “checkout session”; your server makes one HMAC-signed call to Zennopay and returns the pair the SDK needs. There is nothing to sign but the API call itself: you create a payment intent server-to-server, and Zennopay mints the session token and returns it in the same response. No RS256 key pair, no iss/kid, no JWKS — one credential, the HMAC secret. The SDK never sees your HMAC secret. It holds only the session token — a credential Zennopay minted, scoped to a single intent, that expires in a few minutes.

What you need

From the Zennopay Console → Developers tab:
No signing key pair. There is no partner-held RSA keypair and nothing to publish at a JWKS endpoint — Zennopay mints the session token.

Step 1 — Create the payment intent

POST /v1/payment_intents, HMAC-signed (full signing spec: Authentication → Server-to-server HMAC). The body carries your opaque user ID, the authorized USD amount in cents, the corridor, and your per-payment KYC + sanctions attestations:
partner_user_id must be your internal, opaque identifier — never a raw government ID (passport number, national ID). Zennopay enforces per-user regulatory limits against this ID (Vietnam: ₫5,000,000/transaction, ₫10,000,000/day, ₫25,000,000/month), so it must be stable per user.
Send an Idempotency-Key header (a UUID) so a network retry can’t create two intents.
Why attestations are in the create body. You own KYC and sanctions screening for your users; Zennopay owns them for merchants. Because Zennopay mints the session token, the attestation travels with the signed create call — explicit, per-payment, and auditable. Zennopay will not move money on an intent that doesn’t carry them. id_type / id_country declare which government ID your KYC bound this user to; the raw ID number itself never crosses.

Step 2 — Return the session token Zennopay minted

The 201 response carries the session token. There is no minting step on your side — read it off the response and hand it to your app:

Reference implementation (Node.js)

An Express integration example using express and Node’s node:crypto. The application helpers below are placeholders for your authentication, wallet authorization, durable checkout store, and intent-ownership checks. Use the partner starter for a runnable backend. getOrCreateCheckoutOperation must bind a checkout attempt to the authenticated user, persist one idempotency key, and return the same authorized amount and attestations on retries. saveCheckoutIntent records the intent’s ownership before the session is returned to the app.
The refresh route is what your app’s refreshSession hook calls when the SDK hits a 401 mid-flow: same intent, a fresh token from POST /v1/payment_intents/:id/session, fresh expiry window. Zennopay preserves the scan/quote/confirm state across the re-mint.

Security notes

  • Short-lived, single-use, intent-bound. If a session token leaks (device log, crash report), it authorizes at most one confirm, on one intent, for a few minutes — and the SDK never puts it in a URL, so it can’t leak via history or referrers.
  • Zennopay-minted. You never hold a session-signing key, so there’s no private key to protect, rotate, or leak. The token is opaque to you.
  • The HMAC secret stays server-side. Only HMAC-signed, server-to-server calls can create intents or re-mint tokens. The mobile app can only do what the session token allows.
  • Attest after auth, not before. Only create an intent for a user who is logged in, KYC-verified, and sanctions-screened right now — the attestations you send are per-payment statements, not cached facts.

Receipt tokens

To let a user reopen the authoritative receipt for a past payment — its live status and the Zennopay brand — you fetch a second, distinct credential: a receipt token. Like the session token, Zennopay mints it; you request it with one more HMAC-signed call. Request one on demand when the user taps a history row (you pass that row’s intent id; Zennopay derives the user from the stored intent). The reference zennopay-partner-starter (v0.2.0+) ships the route as POST /receipt-token { intent_id } → { receipt_token, expires_at }. The full flow — request, presentReceipt, pending-poll, refunded state, and the cross-user 404 — is in Reopen a receipt.

Next steps

Present the PaymentSheet

Hand the session to the SDK on iOS, Android, Flutter, or React Native.

Reopen a receipt

Fetch a receipt token and reopen the authoritative receipt for a past payment.

Test your integration

Drive the whole flow in the sandbox before going live.

Authentication

The full HMAC signing spec and the session-token contract.

QR Payments limits

The corridor limits enforced against your partner_user_id.