PaymentResult to your callback. The UI is Jetpack Compose; scanning is
CameraX + ML Kit, on-device.

The PaymentSheet, end to end, presented from the sample wallet app.
Requirements
minSdk24 (Android 7.0),compileSdk34- Kotlin 1.9+, AndroidX
- Your app does not need to use Compose — the sheet runs in its own Activity. A Compose-first entry point is provided for hosts that are.
- A backend session endpoint that creates the intent and returns the Zennopay-minted session token
Install
mavenCentral() in your settings.gradle.kts (the
Android default) resolves it. The Gradle coordinate is in.zennopay:sdk; the
Kotlin import namespace stays com.zennopay.sdk.
Camera permission
The SDK’s manifest already merges in<uses-permission android:name="android.permission.CAMERA" />
(with the camera marked as an optional feature), and the SDK requests the
runtime permission itself when the scanner opens. You add nothing to your
manifest — just declare camera use in your Play Store listing.
If the user denies the permission, the sheet degrades to a paste-QR-data
fallback — the flow is always completable without a camera.
Configure
The environment is a value, not a code path:Already shipped on
zennopay.in? Nothing to do. Previously published SDK
versions resolve SANDBOX / PRODUCTION to the api.zennopay.in hosts, and
those hosts continue to be served — same API, same credentials. You do not
need to ship an app update to stay working. See
Environments.Zennopay.configure(config) — per-call config still overrides it.
Your API keys never live in the app: per payment, your backend
creates the intent and Zennopay returns a session
token. Your backend passes it to your app, and the SDK holds only that
short-lived token.
Present the PaymentSheet
With a checkout session from your backend in hand, your app makes one call. Two integration patterns:Pattern 1 — Compose launcher (recommended)
For Compose hosts,rememberZennopayLauncher binds your result callback and
config once and hands you a stable present lambda — safe to pass straight to
click handlers, stable across recompositions:
Pattern 2 — one-shot presentCheckout
From any ComponentActivity (Views or Compose), call the entry point directly:
Intent, so it can’t leak via logcat or recents.
Handle the result
ZennopayError is a sealed taxonomy with stable code strings
(InvalidJwt, IntentMismatch, JwtExpired, MalformedToken,
InvalidIssuer, quote/confirm/network cases, …) — exhaustive in a when.
On a timeout the payment is effectively pending — it may still settle;
reconcile via your webhook or
GET /v1/payment_intents/:id rather than assuming a terminal failure.
Customize appearance
ZennopayAppearance themes the sheet to your brand — colors, corner radii,
typography, primary button, and an optional header logo:
ZennopayAppearance.Automatic) and the sheet uses the default
Zennopay look, following system light/dark.
Structural rules are not overridable: corner radii are clamped to ≤ 12 dp,
amounts always render in tabular figures, and the accent color is reserved
for state signals.
Test
- Emulator: the emulator’s virtual camera won’t decode a real QR reliably — use the sheet’s paste-QR fallback (or the emulator’s virtual-scene camera image) with any VietQR payload string. The backend does the authoritative parse either way.
- Sandbox end-to-end: run
ZennopayConfig.SANDBOXwith a session from your sandbox backend; the payout runs on the sandbox rail and the intent reachescaptured. - Fail-fast checks: pass a blank or mismatched session token and assert an
immediate
Failed(InvalidJwt)/Failed(IntentMismatch)with no UI. - Physical device: camera scanning and the runtime-permission prompt are the pre-release checklist.
Under the hood (what you can rely on)
- The session token is held in memory and sent as
Authorization: Beareron every REST call — never placed in a URL or anIntentextra. - Slide-to-pay fires
POST /confirmexactly once; the idempotency key is persisted (DataStore) before the network call, so retries and process death can never double-debit. - On relaunch mid-payment, the SDK recovers the true terminal status with a
GETrather than re-confirming.
ProGuard / R8
The SDK ships consumer rules; no additional configuration is needed.Next steps
Build your session endpoint
The one backend route the sheet needs.
Test your integration
The sandbox loops to run before release.
Webhooks
Reconcile terminal states server-side.
Per-user limits
The corridor limits Zennopay enforces for you.