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:
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
The201 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 usingexpress 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.
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.