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

# Issue and deliver a wallet pass end to end

> Run a pass through its full lifecycle — issue a loyalty card, deliver the save link in an SMS, WhatsApp, or email message, update the points balance, and void it — plus the campaign patterns and production checklist.

# Issue and deliver a wallet pass end to end

The [Wallet passes channel reference](/channels/wallet-passes) covers the endpoint contract. This guide is the working pipeline most integrations need: issue a pass, put its save link in a message your customer can read, keep the pass content current, and retire it cleanly. Follow it once and the same sequence works whether you issue loyalty cards, coupons, or event tickets.

## Pass lifecycle at a glance

Every pass follows the same three-stage lifecycle, and two properties keep the pipeline safe to automate:

* **Issue** → **update** → **void**. Issued passes start `active`; updates patch an active pass; voiding is terminal.
* **Generation counter.** Every accepted update increments `generation`. That counter is the holder-side signal — the wallet app on the holder's phone polls by generation and refreshes the stored pass whenever it moves. Each new balance is visible without the holder re-saving anything.
* **Idempotency.** Supply an `idempotency_key` at issue time and a retried call returns the original pass with `replayed: true` instead of minting a duplicate. Build one key per enrollment, order, or enrollment-event id.

## Prerequisites

* **An API key with `contacts:write`** (read operations accept `contacts:read`). Create or edit keys in the dashboard under Settings → API keys. The final step sends a message, so the key also needs message-send permission.
* **A contact with a deliverable address.** Passes are issued against a contact — create or import the contact first ([import guide](/guides/import-contacts)), and make sure the contact has an E.164 phone number or an email address on file depending on the channel you plan to deliver over.
* **Platform credentials connected, eventually.** You can issue passes with neither Apple nor Google connected — content is stored, and the `save_url` / `pass_json` payloads start working the moment credentials connected, no re-issuance needed. Check status first:

```bash theme={null}
curl https://api.orbit.devotel.io/api/v1/wallet-passes/platforms \
  -H "X-API-Key: $ORBIT_API_KEY"
```

## Step 1 — Issue a loyalty card

Create the pass with its barcode, display fields, and an idempotency key tied to whatever event enrolled the customer:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/wallet-passes/issue \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "loyalty_card",
    "contact_id": "ctn_9f8e7d6c5b4a",
    "title": "Aurora Coffee Rewards",
    "subtitle": "Gold tier",
    "organization_name": "Aurora Coffee",
    "barcode_message": "MEMBER-10482",
    "barcode_format": "QR_CODE",
    "background_color": "#1E3FB0",
    "fields": [
      { "key": "points", "label": "Points", "value": "0" },
      { "key": "member_since", "label": "Member since", "value": "2026" }
    ],
    "idempotency_key": "enroll-10482-2026-08"
  }'
```

Two fields from the response matter for the next step: `data.id` (the `wps_…` pass id you update and void against) and `data.platforms.google.save_url` (the delivery link). Save both. For Apple Wallet holders, the delivered payload is the signed pass served once Apple credentials are connected; the Google link covers Android holders today.

<Note>
  If any retry of the issue call can happen — a queue worker, a backfill, a webhook consumer with at-least-once delivery — keep the `idempotency_key`. Without it, a retry mints a second pass, and both become visible on the customer's phone.
</Note>

## Step 2 — Deliver the save link

The pass is only useful once the customer has it. Put the save link in the channel they opted into — SMS, WhatsApp, or email all work the same way:

```bash theme={null}
SAVE_URL=$(jq -r '.data.platforms.google.save_url' issue-response.json)

curl -X POST https://api.orbit.devotel.io/api/v1/messages \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"to\": \"+14155552671\",
    \"channel\": \"sms\",
    \"content\": {
      \"text\": \"Welcome to Aurora Rewards! Add your card to your phone wallet: ${SAVE_URL}\"
    }
  }"
```

Save links are long. If SMS character count matters, shorten the URL through the [links endpoint](/api-reference/links) first — the same pattern applies on WhatsApp and email, where a longer raw URL is acceptable and the message body has more room. On email, link the "Add to wallet" button in your template instead of printing the URL in the body.

## Step 3 — Update the pass content

Loyalty and coupon passes change state: points move, tiers shift, expiries extend. Update after every earn or spend event — only the fields you send change; everything else carries forward:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/wallet-passes/wps_a1b2c3/update \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "fields": [
      { "key": "points", "label": "Points", "value": "1,240" }
    ]
  }'
```

Each accepted update pushes `generation` forward, and the holder's wallet app refreshes the pass when the counter moves — a customer who saved the pass sees the new balance without saving again. To remove a field entirely, send it as explicit `null`. Read back `GET /wallet-passes/:id` if you need to confirm the generation you last accepted.

## Step 4 — Void the pass

When a membership is cancelled, an order is refunded, or a coupon is redeemed, void the pass rather than leaving stale collateral on the customer's phone:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/wallet-passes/wps_a1b2c3/void \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Membership cancelled" }'
```

Void is terminal. The pass keeps its content for audit, reports `status: "voided"`, and renders expired in the wallet. Any further update — or a second void attempt — is rejected with a `409` and a `CONFLICT` code. If the customer re-enrolls or re-orders, issue a fresh pass; never try to revive a voided one.

## Campaign patterns

Three delivery flows cover most wallet-pass usage:

**Welcome series.** Enroll the customer into the loyalty program from a signup event, issue the pass immediately with the membership id as `idempotency_key`, then send the save link as the second step of the welcome series. Numbers stay safe to re-run end to end.

**Event tickets.** Issue `type: "ticket"` with the order id as `external_id`, plus seat/gate/order fields. If the seat assignment changes, one update moves the gate label — the holder's wallet refreshes by generation. On order refund or ticket transfer, void the old pass and issue the replacement.

**Coupons.** Issue `type: "coupon"` with `expires_at` set to the offer deadline and a redemption barcode the POS scanner reads. Void the pass once the redemption is confirmed — or let `expires_at` expire it naturally — and keep the reason field informative so the audit list reads cleanly.

## Production checklist

Before going live with a wallet-pass pipeline:

* **Barcode format matches the scanner.** POS scanners differ — thermal scanners read linear `CODE_128` better; mobile readers prefer `QR_CODE`. Test against the actual hardware before a bulk issue.
* **Expiry handling is decided.** Coupons should always carry `expires_at`; loyalty memberships usually should not. Wallet apps surface expiry to the holder, so leaving it blank on an offer is a support-ticket source.
* **Idempotency keys are per-event.** Key them on the enrollment or order id, not on the contact id, so a re-issue after a void mints a genuinely new pass.
* **Platform status is checked before bulk delivery.** A paused credential connection during the send means customers receive a message with no usable link. Gate the batch on `GET /wallet-passes/platforms`.
* **Balance stays live.** Loyalty passes go stale fast — update the `points` field after each earn/spend event so the wallet is the customer's source of truth.
* **Void semantics are respected in code.** Treat `409 CONFLICT` from update or void as expected termination, not a retryable error. Loop-retrying a voided pass is the most common wallet-pass bug.

The full field contract and error table stay in the [Wallet passes channel reference](/channels/wallet-passes); the dashboard issuance builder under **Marketing → Wallet passes** covers the same lifecycle for operators.
