Skip to content
Ask the docs

Find answers across the QairoPay docs.

Type a question and we'll synthesize an answer from the docs with citations back to the source pages.

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
Pass states and the transitions that move between them. Webhook event names follow the arrow.

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/[email protected]",
icon_url: "https://acme.example/[email protected]",
},
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:

  • kind governs the wallet category. Options: loyalty, membership, coupon, event_ticket, boarding_pass, gift_card.
  • Field display maps to Apple Wallet’s primary/secondary/auxiliary slots and Google Wallet’s structured fields. The renderer picks the right layout per platform.
  • barcode.payload_template is a server-side template. Variables in {...} are substituted at issuance time with the pass’s actual data.
  • relevant_locations trigger 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: "[email protected]", name: "Jane Doe" },
fields: { tier: "Platinum", points: "12450", member_since: "2024" },
},
{ idempotencyKey: crypto.randomUUID() },
);
console.log(pass.download.apple_url); // signed .pkpass URL
console.log(pass.download.google_url); // Google Wallet save URL

You’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.scanned event 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:

Terminal window
qairopay promote pass-template tpl_01HZX --to live

See Sandbox vs live for the full promotion semantics.