Skip to main content
Your app already keeps its own transaction history: you consume GET /v1/transactions (or your own ledger) and render the list your way. What you don’t have to build is the receipt — the authoritative, branded detail view for a single past payment, with its live status (still pending, captured, failed, or refunded). When a user taps a history row, fetch a short-lived receipt token from Zennopay (one HMAC-signed call) and hand it to the SDK’s presentReceipt. The SDK fetches the authoritative receipt from Zennopay, renders it natively, and — if the payment is still settling — polls it to a terminal state in place.
The Zennopay receipt screen: status, amount paid, USD debited, merchant, masked destination account, transaction ID, and timestamp.

The authoritative Zennopay receipt, reopened from a history row in the sample wallet app.

You own the list, we own the receipt. Your history list is yours — cached, paginated, styled to your app. The receipt surface is the SDK’s: one call reopens the same branded receipt your user saw at pay time, but refreshed to the payment’s current state. No receipt UI, no status mapping, no refund copy to build.

The flow

The receipt token is fetched on demand, only when the user opens a specific receipt — not ahead of time for the whole list.

The receipt token

A receipt token is a distinct credential from the session token you use to make a payment. Like the session token, Zennopay mints it — you request one with a single HMAC-signed call to POST /v1/payment_intents/{intent_id}/receipt_token (no body). Its scope, power, and lifetime are different: a session token authorizes one debit on one intent; a receipt token only reads a receipt.
How a receipt token differs from a session token:
  • Minted per intent, scoped to a user. You mint it by naming an intent you own; Zennopay derives the user (sub) from that stored intent, so the token can open any of that user’s receipts within its window — but never another user’s.
  • Read-only. It authorizes GET /receipt and nothing else — it can never confirm, cancel, or move money.
  • Reusable. The SDK reuses the one token to poll a pending receipt to its terminal state, so you don’t re-mint on every poll.
  • No attestations. KYC/sanctions are a pay-time contract; reopening a receipt doesn’t move money, so they aren’t required.

Fetch the receipt token

Add one route to your session endpoint backend. It makes one HMAC-signed call to Zennopay — no body — for the intent id of the tapped row, then returns the token. Reuse the same hmacHeaders helper from your session endpoint; no dependencies beyond node:crypto.
The reference zennopay-partner-starter (v0.2.0+) ships this route as POST /receipt-token { intent_id } → { receipt_token, expires_at }. Use it as the reference.

Fetch is the SDK’s job

The SDK calls GET /v1/payment_intents/:id/receipt with the receipt token as Authorization: Bearer. You usually don’t call this endpoint yourself — presentReceipt does. It returns the authoritative receipt with a live status (pending, captured, failed, or refunded) and the branded receipt fields.
Cross-user isolation. The endpoint returns 404 for an intent that doesn’t belong to the token’s sub — another partner’s intent, or another of your users’ intents. It does not distinguish “doesn’t exist” from “not yours”, so it leaks no information about intents outside the current user.
See GET /payment_intents/:id/receipt for the full response shape.

Present the receipt

Each platform exposes one presentReceipt call. Pass the intentId from your history row and the freshly minted receiptToken; give it a refreshReceiptToken hook so it can re-mint if the token expires while a slow payout is still being polled.
Zennopay 0.3.0+

What the SDK handles for you

  • Pending → terminal polling. If the payment is still settling, the receipt opens in a processing state and the SDK polls GET /receipt (reusing the same token) until it reaches captured or failed, updating the receipt in place. A slow payout gets honest copy, not a silent spinner.
  • Refunded state. A refunded receipt renders refund messaging — the amount returned and when — instead of a plain success screen, so a user reopening an old payment sees the current truth.
  • Cross-user 404. A token for user A can’t open user B’s receipt: the endpoint returns 404 with no existence leak, and the SDK surfaces a not-found state rather than another user’s data.
  • Branding. The receipt honors the same ZennopayAppearance you pass to the PaymentSheet, so a reopened receipt matches the one shown at pay time.

Next steps

Build your session endpoint

The receipt-token route lives alongside your session endpoint.

GET /payment_intents/:id/receipt

The authoritative receipt response shape and the no-leak 404.

Webhooks

Reconcile terminal states server-side, independently of the receipt.

PaymentSheet overview

The pay-time flow the receipt mirrors.