Skip to main content

Wallet pass lifecycle

A wallet pass in Orbit is a digital card — a loyalty card, coupon, or event ticket — rendered for Apple Wallet and Google Wallet from one platform-agnostic content model. This page explains the lifecycle semantics: the three pass types and when to choose each, the issue → update → void state machine, the generation counter that tells holders to refresh, the append-only ledger the state is projected from, and how delivery works. For the step-by-step issuance workflow and full field reference, see the Wallet Passes channel guide; for the architecture this lifecycle shares with incentives and loyalty, see the event-ledger projection model.

The three pass types

Every issue call picks one of three pass types — the type is fixed for the pass’s lifetime and chooses the layout each wallet renders: The type drives one thing — the visual layout. Barcodes, colors, fields, expiry, and the lifecycle all behave identically across the three. Check GET /wallet-passes/platforms for the supported types list before building your own issuance UI.

The lifecycle: issue → update → void

A pass moves through exactly one path:
  1. Issue. POST /wallet-passes/issue stores the full content snapshot and returns both platform payloads — an Apple pass.json structure and a Google “Save to Wallet” link — with status: "active" and generation: 0. A fresh issue returns 201; a replayed issue (same idempotency key) returns 200.
  2. Update. POST /wallet-passes/:id/update patches an active pass’s content — a points balance, a new tier, a coupon expiry, a seat change. Only the fields you send change; everything else carries forward, and sending a field as explicit null clears it. Each accepted update bumps generation by one.
  3. Void. POST /wallet-passes/:id/void permanently revokes the pass, optionally recording a reason. Void is terminal: the pass reports status: "voided", renders as expired in the holder’s wallet, and any further update or re-void returns 409 CONFLICT naming the current status. A voided replacement is always a new pass — issue again.
Both update and void run under a per-pass advisory lock, so two concurrent transitions on the same pass apply in a well-defined order; the loser gets the 409 against the post-transition state.

The generation counter

generation is the pass-changed signal, not a version to diff. It starts at 0 at issue and increments by exactly one on every accepted update. Compare it across polls to learn “something moved” without diffing content fields, and treat each bump as the trigger a wallet push-registration step would key off to tell Apple or Google Wallet to refresh the holder’s copy. Because reads fold the full event history at request time, the counter — like everything else on a read — is current, never cached.

The append-only ledger

Pass state lives in the tenant’s append-only event store — the same store the CDP, loyalty balances, and incentive issuance use. Issue appends a wallet_pass.issued event carrying the full content snapshot; each update appends a wallet_pass.updated event carrying only the changed fields; void appends a terminal wallet_pass.voided event. The pass you read is a projection folded from that stream on every request — no separate table, no migration, no background job that can fall behind. What that buys you:
  • Audit. The issued event keeps exactly what the holder saved, forever. “What did this coupon say when the customer installed it?” is answered by the original event, not reconstructed from edits.
  • No drift. A GET returns the fold computed at request time; there is no sync layer between storage and response.
  • Safe transitions. Update and void re-read, validate, and append inside one transaction under the per-pass lock.

The idempotency key

Issuance is idempotent. Send an idempotency_key in the body (or an Idempotency-Key header), and a retry returns the first issued pass with replayed: true instead of minting a duplicate — the key lookup and the append run inside one transaction, so a double-submit or a client retry can never create two passes for one enrollment. The replay is folded to the pass’s current lifecycle state: if the original pass was updated or voided between the first attempt and the retry, the replayed response reflects that — it never reports stale issued-time content. Issuing returns the delivery artifacts in the response; there is no separate “send” step inside the wallet-passes API.
  • Google Wallet produces a platforms.google.save_url — an RS256-signed “Save to Google Wallet” link you drop into an SMS, WhatsApp, or email message. Opening it adds the pass to the holder’s wallet.
  • Apple Wallet produces platforms.apple.pass_json — the unsigned pass structure. The binary signature step (a PKCS#7 signature over the .pkpass bundle) runs on the transport that holds the Apple signing certificate, connected via your Apple developer account.
Save links are long — route them through link shortening for a short URL in SMS. Delivery itself uses the standard messaging endpoint; the move is “issue pass → take save_url → send it as the message body.” A worked example lives in the channel guide.

Where passes end: boundaries with loyalty and incentives

The pass is the carrier, not the program or the payout:
  • Loyalty. Points balances, tiers, earn and burn rules live in the loyalty program model. The loyalty card pass is the surface a member installs; the program is what earns and burns the points the card displays. Rendering the wallet-pass label is one of several tier renderers.
  • Incentives. Redemption — minting a discount code, calling a gift-card network, recording a manual payout — routes through the incentives fulfillment engine. The pass shows the reward; incentives pay it out.
  • Lifecycle. The issue/update/void machine documented here applies to passes only. Loyalty config resets and incentive voids are separate surfaces following the same ledger pattern (see the projection model).

Example: the lifecycle in three calls

Idempotent issue (a retry with the same key returns 200 + replayed: true, not a second pass):
Update — only the fields you send change; explicit null clears a field; generation increments:
Void — terminal; a further update or re-void returns 409 CONFLICT:
Update and void are safe to retry: the loser of a race — or a second attempt — gets a deterministic 409 CONFLICT naming the pass’s current status.

Platforms and barcode formats

Two platforms, four barcode symbologies: GET /wallet-passes/platforms reports each platform’s configured flag (with a machine-readable reason when not connected), the supported types, and the barcode_formats. You can issue and manage passes with no platform connected — the content is stored and readable — and the downloadable payloads appear the moment credentials are connected, because every read rebuilds them fresh.