> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Wallet pass lifecycle: issue, update, void

> What the wallet-pass state machine means: the three pass types and when to choose each, the issue → update → void lifecycle, the generation counter as the holder-refresh signal, the append-only ledger under an idempotency key, and where passes end versus loyalty and incentives.

# 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](/channels/wallet-passes); for the architecture this lifecycle shares with incentives and loyalty, see the [event-ledger projection model](/concepts/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:

| Type           | Choose when                                                                      | Rendered as                                          |
| -------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `loyalty_card` | The pass carries an ongoing membership: points balance, tier, membership number. | Apple `storeCard` layout; a Google generic object.   |
| `coupon`       | The pass is an offer with a redemption barcode and usually an expiry date.       | Apple `coupon` layout; a Google generic object.      |
| `ticket`       | The pass admits the holder to an event: seat, gate, order details.               | Apple `eventTicket` layout; a Google generic object. |

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:

```text theme={null}
issue ──▶ active ──▶ (update)* ──▶ voided (terminal)
```

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](/concepts/cdp-event-model), 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.

## Delivery: the save link is just a link

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](/api-reference/links) 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](/channels/wallet-passes#example-enroll-a-loyalty-pass-from-an-sms-deep-link).

## 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](/concepts/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](/concepts/incentives). 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](/concepts/event-ledger-projection-model)).

## Example: the lifecycle in three calls

Idempotent issue (a retry with the same key returns `200` + `replayed: true`, not a second pass):

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/wallet-passes/issue \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "type": "coupon",
    "title": "20% off spring collection",
    "barcode_message": "SPRING20-C4X9",
    "barcode_format": "QR_CODE",
    "expires_at": "2026-06-30T23:59:59Z",
    "idempotency_key": "coupon-spring20-batch-3"
  }'
```

Update — only the fields you send change; explicit `null` clears a field; `generation` increments:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/wallet-passes/wps_.../update \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "title": "25% off spring collection",
    "subtitle": null
  }'
```

Void — terminal; a further update or re-void returns `409 CONFLICT`:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/wallet-passes/wps_.../void \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Offer withdrawn" }'
```

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:

| Platform      | Live when                                         | Barcode formats                                    |
| ------------- | ------------------------------------------------- | -------------------------------------------------- |
| Apple Wallet  | Pass-type id + team id connected                  | `QR_CODE`, `PDF_417`, `AZTEC`, `CODE_128`          |
| Google Wallet | Issuer id + service-account credentials connected | Same four — the enum names both platforms agree on |

`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.

## Related reading

* [Wallet Passes channel guide](/channels/wallet-passes) — the step-by-step issuance workflow and field reference.
* [The event-ledger projection model](/concepts/event-ledger-projection-model) — the shared ledger architecture passes, incentives, and loyalty project over.
* [Loyalty program model](/concepts/loyalty-program-model) — the points-and-tiers program a loyalty-card pass carries.
* [Incentives: catalog, ledger, and fulfillment](/concepts/incentives) — the engine pass redemptions route through.
* [Idempotency and safe retries](/concepts/idempotency-and-safe-retries) — the retry discipline the idempotency key extends.
