Issuing your first pass
This guide walks through a full Pass program lifecycle. It assumes you’ve completed the Quickstart and have a sandbox key. By the end, you’ll have a templated, branded, signable, scannable pass program ready to promote to live.
Pass lifecycle at a glance
Every pass moves through this state machine. Transitions are driven by API calls (POST /v1/passes, PATCH /v1/passes/{id}, POST /v1/passes/{id}/revoke) and by holder actions on the device (install, remove). Each transition emits a webhook — see the event catalog for the full list.
stateDiagram-v2
direction LR
[*] --> Issued: POST /v1/passes
Issued --> Installed: holder installs\n→ pass.installed
Issued --> Removed: holder declines /\nlink expires\n→ pass.removed
Installed --> Installed: PATCH /v1/passes/{id}\n→ pass.updated
Installed --> Scanned: tap or scan\n→ pass.scanned
Scanned --> Installed: scan complete
Installed --> Revoked: POST .../revoke\n→ pass.revoked
Issued --> Revoked: POST .../revoke
Revoked --> [*]: 90-day archival
Removed --> [*]: 90-day archival 1. Design the template
A pass template is your brand mold. It defines the visual treatment, the structured fields, the barcode/NFC configuration, and the lifecycle rules. Issue many passes from one template; update the template to push updates to every issued pass.
const template = await qp.passTemplates.create({ name: "Acme Hotels — Loyalty", kind: "loyalty", brand: { background_color: "#1F7A5A", foreground_color: "#FCFAF6", logo_url: "https://acme.example/logo@2x.png", icon_url: "https://acme.example/icon@2x.png", }, fields: [ { key: "tier", label: "Tier", value: "Gold", display: "primary" }, { key: "points", label: "Points", value: "0", display: "secondary", numeric: true }, { key: "member_since", label: "Member since", value: "—", display: "auxiliary" }, ], barcode: { format: "qr", payload_template: "loyalty:{pass.id}" }, nfc: { enabled: true, payload_template: "loyalty:{pass.id}" }, relevant_locations: [ { latitude: 28.4734, longitude: -81.4683, radius_m: 500, message: "Welcome to the Resort." }, ],});A few notes:
kindgoverns the wallet category. Options:loyalty,membership,coupon,event_ticket,boarding_pass,gift_card.- Field
displaymaps to Apple Wallet’s primary/secondary/auxiliary slots and Google Wallet’s structured fields. The renderer picks the right layout per platform. barcode.payload_templateis a server-side template. Variables in{...}are substituted at issuance time with the pass’s actual data.relevant_locationstrigger geofencing notifications (e.g., “Welcome to the Resort”) on Apple Wallet and Google Wallet platforms that support them.
2. Issue a pass
const pass = await qp.passes.create( { template_id: template.id, holder: { email: "jane@example.com", name: "Jane Doe" }, fields: { tier: "Platinum", points: "12450", member_since: "2024" }, }, { idempotencyKey: crypto.randomUUID() },);
console.log(pass.download.apple_url); // signed .pkpass URLconsole.log(pass.download.google_url); // Google Wallet save URLYou’ll typically email or SMS the appropriate URL to the holder. The same pass.id is valid across both wallets; QairoPay handles the divergent provisioning details.
3. Update fields
When the holder’s status changes (tier upgrade, points balance, expiry), patch the pass. Updates fan out to all installed instances:
await qp.passes.update(pass.id, { fields: { tier: "Diamond", points: "23010" },});By default, updates trigger a silent push to the device. To push with a user-visible notification, add change_message:
await qp.passes.update(pass.id, { fields: { tier: "Diamond" }, change_message: "Welcome to Diamond — three free nights await.",});The change_message appears on the lock screen on iOS and as a Google Wallet notification on Android.
4. NFC and scanning
If your program has physical interaction (hotel doors, theme-park entry, in-store redemption), tap-to-redeem is the dominant pattern.
- Server-side: NFC payloads are signed by QairoPay using your tenant’s NFC envelope key. Scanners verify offline with the public side of that key.
- Scanner SDK: download the platform-specific SDK from the dashboard. iOS, Android, and Linux POS builds are available.
- Audit trail: every scan emits a
pass.scannedevent with the scanner id, lat/long, and verification result.
qp.passes.scans.list({ pass_id: pass.id, limit: 10 });See the POS adapter reference for the full integration.
5. Revoke when needed
Lost devices, churned members, refunded purchases — revoke the pass:
await qp.passes.revoke(pass.id, { reason: "lost_device" });Revoked passes are voided in the wallet (Apple Wallet greys them out and adds a “Voided” stamp; Google Wallet hides them). Scanners reject revoked passes; the corresponding pass.revoked event fires immediately.
6. Pushing template-wide updates
Updating the template itself updates every issued pass instantly:
await qp.passTemplates.update(template.id, { brand: { background_color: "#2A3A5E" },});This re-signs all issued passes and pushes the new artwork to every installed device. Use this for brand refreshes; use per-pass updates for per-holder data changes.
Promoting to live
When your sandbox program is right, promote the template to live and re-issue:
qairopay promote pass-template tpl_01HZX --to liveSee Sandbox vs live for the full promotion semantics.
What to read next
- Webhooks → Event catalog — every event your Pass program emits.
- Spend Card guide — pair a pass with a debit card that settles in USDC.
- Errors — the codes specific to pass issuance and lifecycle.