Skip to main content

Wallet passes

Orbit can issue digital passes for Apple Wallet and Google Wallet: loyalty cards, coupons, and event tickets your customers save to the wallet app on their phone. Issue them through the API or from the dashboard’s issuance builder, and deliver them by putting the pass’s save link in any message you send — SMS, WhatsApp, or email. For the lifecycle semantics behind these steps — the state machine, the generation counter, and the ledger — see the wallet pass lifecycle concept. Three pass types are supported today. Pick the type first, because it decides how the holder redeems it:
  • Loyalty cards — points balance, tier, or membership number behind a scannable barcode. Reach for this when the pass grants standing status: reward membership, tier, or a member number the staff scans at checkout.
  • Coupons — an offer with a redemption barcode and an optional expiry date. Reach for this when the pass is the discount a holder redeems once — a percentage off, a free item, a gift-with-purchase. Set expires_at so the offer dates itself.
  • Event tickets — a one-time or dated piece of access a holder gets admitted with: seat, gate, order number. Reach for this when the staff needs to scan to admit, not to look up a balance.
If none of these fit — you want a keyed balance or an identity artifact rather than a scannable token — a wallet pass is the wrong channel and a link in a message is usually enough.

The pass lifecycle

Every pass moves through the same lifecycle:
  1. Issue — POST /wallet-passes/issue creates the pass and returns both platform payloads (an Apple pass.json structure and a Google “Save to Wallet” link). New passes start active.
  2. Update — POST /wallet-passes/:id/update patches an active pass’s content (points balance, tier, coupon expiry, ticket seat). Each accepted update bumps the pass’s generation counter, which is the signal that the holder should refresh the pass on their phone.
  3. Void — POST /wallet-passes/:id/void permanently revokes an active pass. Voiding is terminal: a voided pass reports status: "voided" and rejects any further update or re-void with a 409 CONFLICT error.
Request body (issue):

Issuing a pass

The same issue call in four languages:

Apple vs Google templates

One issue call produces both platform payloads. The same inputs resolve differently per platform, so know how each renders them:
  • Apple Wallet (pass.json) — title maps to the primary field; the first four entries of fields become secondary fields rendered under it. Apple types pick their visual template by class — a coupon scans like a coupon, a ticket like a ticket — so send the real type even where the fields overlap.
  • Google Wallet (generic object) — title becomes the card header, subtitle the subheader, and each fields entry becomes a text module row rendered as label + value, so all fields show without the four-item cap Apple applies.
Design the pass content once and let each platform style it. When the pass makes an offer, choose your type by the holder’s redemption behavior, not which platform renders it — both platforms agree on the QR_CODE / PDF_417 / AZTEC / CODE_128 barcode formats, so the encoded value is portable.

Response

Issuing is idempotent: pass an idempotency_key (or an Idempotency-Key header) and a retry returns the original pass with replayed: true instead of minting a second one. The replayed response reflects the pass’s current state — so a delayed retry after an update or void never reports stale content.

Platform status

Before issuing, check which platforms are live:
  • apple — configured: true when the Apple Wallet pass-type id and team id are connected. Until then pass_json is null, because there is no valid certificate identity to stamp the pass with.
  • google — configured: true when the Google Wallet issuer id and service-account credentials are connected. Until then save_url is null.
  • types / barcode_formats — the enums the issuance form can offer; use these to drive your own UI validation.
You can issue and manage passes with no platform connected — the pass content is stored and readable — but the downloadable payloads appear only after the matching platform is connected. Every read rebuilds the platform payloads fresh, so existing passes start producing working save links the moment credentials are connected; you never need to re-issue. Each platform has its own signing mechanism:
  • Apple Wallet builds the pass as a signed .pkpass bundle: a PKCS#7 signature over a manifest of the pass files, using the Apple-issued pass signing certificate tied to your pass type. Orbit generates the unsigned pass.json content (platforms.apple.pass_json); the final binary signing step runs on the transport that holds the certificate material, once the Apple credentials are connected.
  • Google Wallet builds a signed “Save to Google Wallet” link (platforms.google.save_url): the pass is wrapped in a Google generic object and encoded as an RS256-signed token appended to https://pay.google.com/gp/v/save/.... Opening that link on an Android device or in a browser adds the pass to the holder’s Google Wallet account.

Listing, updating, and voiding passes

List issued passes (newest first) with optional filters:
Update an active pass’s content — only the fields you send change; everything else carries forward:
Each update increments generation. To clear a field, send it as explicit null. Void a pass (optionally with a reason):
A voided pass keeps its content for audit, reports status: "voided", renders as expired in the wallet, and cannot be updated.

Example: issue a pass when a commerce checkout completes

The pattern is one pass per completed order, driven off the customer-data event stream: when your CDP ingests a order_completed event from the commerce checkout spine, you trigger the wallet-pass issue call — the spine tells you that the order completed; the pass API records a coupon pass got issued for it.
  1. Ingest the event. When commerce finishes collecting payment, fire the documented event into the CDP following your commerce integration — the order_completed verb carries the order total, currency, channels, and reference. See the attribution-conversion events reference for the event ingestion patterns.
  2. Issue the pass with the order as your correlation key. Point an integration worker — or a flow — at the event and call issue with external_id set to the order reference, so the pass traces back to the purchase that earned it:
    The idempotency_key derived from the order reference matters here: event delivery can retry, and the keyed issue call returns the original pass instead of stacking duplicate coupons.
  3. Send the save link on the channel the buyer used. The commerce spine surfaces still hand you back a contact bound to a channel — SMS, WhatsApp, email. Take data.platforms.google.save_url (or the Apple payload once Apple credentials are connected) and send it through the standard messaging endpoint the same way as the enrollment flow below. Recovery gifting works the same way: when a flagged-abandoned cart recovers and completes, a follow-up coupon pass is a second-touch offer, not a duplicate.
The most common onboarding flow: a customer joins your loyalty program, and you text them their wallet pass.
  1. Issue the pass tied to the enrolling contact:
  2. Pick the save link from the response — data.platforms.google.save_url for Google Wallet holders (the Apple Wallet equivalent is the signed pass the .pkpass transport serves once Apple credentials are connected).
  3. Send the link over SMS using the standard messaging endpoint:
    Keep the message brand-clean — save links are long, so route them through Orbit link shortening if you want a short URL. The same deep-link pattern works on SMS, WhatsApp, or email; pick whichever channel the customer opted into.

The dashboard issuance builder

Operators can run the whole lifecycle without the API. In the dashboard, open Marketing → Wallet passes (also reachable at Outbound → Wallet passes). The surface is gated to owner, admin, and developer roles.
  • Issue a pass — the New pass form offers exactly the pass types and barcode formats GET /wallet-passes/platforms reports, and flags when a platform is not yet connected.
  • Update content — edit points, tiers, or other fields on an active pass; each accepted edit bumps the pass generation.
  • Void a pass — revoke through a confirmation dialog; voided passes remain visible in the list for audit.
  • Copy save links — copy the Google save link (or read the Apple payload status) to drop into a message.
The dashboard surface and the API operate on one shared ledger, so a pass issued from the dashboard is visible to the API and vice versa.

How issuance state is stored

Wallet-pass state lives as events in the same append-only customer-data ledger that powers loyalty programs and incentive issuance. Issuing appends an issuance event; updates and voids append overlay events; and the current state of a pass is folded from its event stream on every read. There is no separate wallet-passes table to migrate, and nothing external to keep in sync — which is also what makes idempotent replays safe: the replayed response folds to the pass’s current lifecycle state. All wallet-pass endpoints are scoped with contacts:read (list/get/platforms) and contacts:write (issue/update/void) — a pass is customer-facing collateral tied to a contact.

Metrics and the ledger events

Every lifecycle step writes a customer-data-platform event, so the pass program is measurable end-to-end without any extra reporting plumbing:
  • wallet_pass.issued — one event per issued pass (the pass-creation counter).
  • wallet_pass.updated — one event per accepted content edit (the generation-bump counter).
  • wallet_pass.voided — one event per terminal revocation.
Query these on the live event-tail debug surface — e.g. GET /cdp/events/debug?event_name=wallet_pass.issued — or export to your warehouse destination as the events roll in. Aggregate them by tenant, contact, or pass type to answer install/redemption-style funnels: issued minus voided is the active pass count; updated divided by issued is how often customers are being re-engaged with refreshed content. Because issuance rides the same ledger a loyalty program or an incentive writes to, a order_completed-driven coupon ties into the same funnel too.

Common errors