Skip to main content

Issue and deliver a wallet pass end to end

The Wallet passes channel reference 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:
  • Issueupdatevoid. 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), 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:

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:
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.
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.
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:
Save links are long. If SMS character count matters, shorten the URL through the links endpoint 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:
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:
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; the dashboard issuance builder under Marketing → Wallet passes covers the same lifecycle for operators.