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:- 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_keyat issue time and a retried call returns the original pass withreplayed: trueinstead of minting a duplicate. Build one key per enrollment, order, or enrollment-event id.
Prerequisites
- An API key with
contacts:write(read operations acceptcontacts: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_jsonpayloads 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: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.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: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: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: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 asidempotency_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_128better; mobile readers preferQR_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
pointsfield after each earn/spend event so the wallet is the customer’s source of truth. - Void semantics are respected in code. Treat
409 CONFLICTfrom update or void as expected termination, not a retryable error. Loop-retrying a voided pass is the most common wallet-pass bug.