API and Integration Guide
Connect your wallet passes to the tools you already use — native integrations, a REST API, and signed webhooks.
Pass Studio lets you design Apple Wallet and Google Wallet passes — coupons, loyalty cards, event tickets, membership cards — and distribute them to your customers. Beyond the studio itself, Pass Studio plugs into your existing stack.
Native integrations
Apple Wallet & Google Wallet
Every pass is delivered natively to both wallets — no app for your customers to install. Passes support live updates: when you change a pass or a customer earns points, the pass on their phone updates with a push and can display a lock-screen notification. Location-aware passes can surface near your store and show one-tap directions.
Shopify
Connect your Shopify store to Pass Studio and:
- Auto-issue passes on paid orders — a customer completes checkout and receives a branded email with official Add to Apple Wallet / Add to Google Wallet buttons. Smart issuance logic avoids sending a pass to customers who already have one.
- Closed-loop loyalty — passes carry unique codes; orders that redeem them accrue points or advance the customer to their next reward automatically.
- Discount-code sync — pass barcodes work as Shopify discount codes at checkout, online and at POS.
Setup is a guided connection from Pass Studio Settings — no code required.
Issuance channels
- Public issuance forms — a hosted form page for each pass; customers enter their details and get a personalized pass with an auto-assigned member ID. Perfect for QR-code signage, link-in-bio, or email campaigns.
- Share links & email — send passes directly with official wallet badge buttons.
POS staff app
A lightweight web app for your staff — scan a customer's pass QR at the counter, see their profile, award points, and the pass on their phone updates before they leave the register. Works on any tablet or phone browser; nothing to install.
REST API
Base URL: https://www.passstudio.online/api/v1
All endpoints accept and return JSON. Errors return a 4xx status with { "error": "…" }.
Authentication
Create an API key in Settings → API Keys, then send it as a bearer token on every request:
Authorization: Bearer ps_live_...A missing or revoked key returns 401. All data access is scoped to the key's account — a key can never read or modify another account's passes or holders.
Endpoints at a glance
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /me | Verify your key and identify the account |
| GET | /passes | List your passes with their editable field keys |
| POST | /passes/{passId}/issue | Issue a pass to a customer (idempotent per email) |
| PATCH | /passes/{passId}/fields | Update field values for all holders of a pass |
| PATCH | /instances/fields | Update field values for one holder, with instant push |
| GET | /holders/find | Find a holder by email, phone, or pass code |
| POST | /loyalty/points | Award or adjust loyalty points |
| POST | /orders/process | Feed an order into the loyalty engine |
| POST | /instances/revoke | Revoke a single pass instance |
| POST | /instances/revoke-all-by-barcode | Revoke every instance carrying a given code |
Endpoint reference
/meVerifies the API key and returns the account it belongs to. Use it as a connection test.
- No parameters.
curl https://www.passstudio.online/api/v1/me \
-H "Authorization: Bearer ps_live_..."{
"id": "account id",
"email": "you@example.com",
"displayName": "Your Name",
"keyLabel": "My integration key"
}/passesLists the account's passes, including each pass's editable field keys — use it to discover valid keys before calling the field-update endpoints.
- No parameters.
curl https://www.passstudio.online/api/v1/passes \
-H "Authorization: Bearer ps_live_..."{
"passes": [{
"passId": "…",
"name": "VIP Membership",
"passType": "coupon",
"status": "active",
"distributionMode": "unique",
"fieldsEditable": true,
"fieldKeys": ["expiry", "tier"],
"templateOwnedFieldKeys": []
}]
}templateOwnedFieldKeys lists keys managed by the pass template — they can only be updated for all holders at once, never per holder.
/passes/{passId}/issueIssues a pass to a customer by email. Mints a unique code per the pass's barcode settings, stamps contact details, and sends a branded email with Apple / Google Wallet buttons.
- email (required) — the customer's email; issuance is deduped per email
- name, phone (optional) — stamped onto the holder profile
- sendEmail (optional, default true) — set false to create the instance without emailing
- resendIfExists (optional) — re-send the wallet email to an existing holder
- fields (optional) — per-issuance field values, e.g. seat or showtime; keys must exist on the pass template, and template-owned keys are rejected
{
"email": "customer@example.com",
"name": "Jane Doe",
"phone": "+1 555 010 0100",
"sendEmail": true,
"fields": { "seat": "12A", "showtime": "Oct 1, 7:30 PM" }
}{
"instanceId": "…",
"passstudioHolderId": "…",
"barcodeContent": "SAVE-8H2K",
"shareToken": "…",
"addToWalletUrl": "https://www.passstudio.online/i/…",
"emailSent": true,
"alreadyExisted": false
}Idempotent: re-issuing to the same email returns the existing instance with alreadyExisted: true — a retried automation never mints a duplicate code. Fields on a dedupe hit are not applied; use PATCH /instances/fields to change an existing holder's values.
Each new issuance costs 1 credit.
/passes/{passId}/fieldsUpdates field values on the pass template — for every holder — and pushes the change to all active wallets (Apple push + Google object update). Can also update the pass's locations.
- Body is a flat { "key": "value" } map, or { "fields": {…}, "locations": […] }
- locations (optional) — 1–3 entries of { latitude, longitude, name?, relevantText? } for lock-screen relevance
- Unknown field keys are rejected with the list of valid keys for the pass
{
"fields": { "expiry": "Dec 31, 2026" },
"locations": [
{ "latitude": 40.7128, "longitude": -74.006, "relevantText": "Welcome back!" }
]
}{
"ok": true,
"pushed": true,
"updatedFields": ["expiry"]
}Pushes are billed like studio pushes (1 credit per push event); if the balance is insufficient the fields still update and the response reports the skipped push.
/instances/fieldsUpdates field values for a single holder — their personal copy of the pass — and pushes the change to their wallet immediately.
- instanceId or barcodeContent (one required) — identifies the holder's instance
- fields (required) — { "key": "value" } map of per-holder values
- Template-owned keys are rejected (update those for all holders via PATCH /passes/{passId}/fields); unknown keys are rejected with the valid-key list
{
"barcodeContent": "SAVE-8H2K",
"fields": { "seat": "14C" }
}{
"ok": true,
"instanceId": "…",
"updatedFields": ["seat"],
"pushed": true
}/holders/findLooks up a customer and returns their pass instances, most-recently-active first, including loyalty state where applicable.
- Exactly one of: ?email= | ?phone= | ?barcodeContent=
- ?passId= (optional) — scope the search to one pass
curl "https://www.passstudio.online/api/v1/holders/find?email=customer@example.com" \
-H "Authorization: Bearer ps_live_..."{
"found": true,
"holderId": "…",
"email": "customer@example.com",
"phone": "+1…",
"instances": [{
"instanceId": "…",
"passId": "…",
"barcodeContent": "…",
"status": "active",
"loyalty": { "points": 40, "state": "earning" }
}]
}A no-match returns 200 with { "found": false, "instances": [] } — never 404 — so search-or-create automations can branch on found.
Returns at most 20 instances.
/loyalty/pointsAwards or deducts loyalty points. Runs through the full loyalty engine: crossing a reward threshold mints the reward code, transitions the pass face, pushes the update, and notifies the customer.
- delta (required) — points to add (positive) or remove (negative); balances floor at 0. Absolute set is deliberately not supported (it races with concurrent orders)
- One identifier: instanceId | barcodeContent | email + passId
- externalRef (optional) — idempotency key; a repeated ref returns the original result with alreadyProcessed: true instead of double-awarding. Use <source>_<externalOrderId> to also collide with orders sent to /orders/process
{
"delta": 25,
"email": "customer@example.com",
"passId": "…",
"externalRef": "pos_receipt_48211"
}{
"newBalance": 105,
"rewardUnlocked": true,
"newBarcodeContent": "RWD-93KF",
"alreadyProcessed": false
}/orders/processFeeds a provider-neutral order into the same engine that powers the native Shopify integration: redemption detection, loyalty accrual, reward progression, and pass issuance — from any commerce platform.
- externalOrderId (required) — your platform's order id
- source (required) — a stable name for the platform, e.g. "woocommerce"
- email, phone, customerId, customerName (optional) — customer identity
- discountCodes (optional) — codes applied to the order; matching precedence is discount code → email → phone
- orderNumber, subtotal, total, currency, items (optional) — order details
- passId (optional) — scope processing to one pass
{
"externalOrderId": "10057",
"source": "woocommerce",
"orderNumber": "#10057",
"email": "customer@example.com",
"discountCodes": ["SAVE-8H2K"],
"subtotal": "42.00",
"total": "45.36",
"currency": "USD",
"items": [
{ "code": "SKU-001", "description": "Espresso beans 1kg", "quantity": 2, "price": "21.00" }
]
}{
"ok": true,
"action": "accrued",
"alreadyProcessed": false
}action is one of redeemed | accrued | advanced | issued | none.
Idempotent per account + source + externalOrderId: a re-sent order returns the original result with alreadyProcessed: true.
/instances/revokeRevokes one pass instance — the pass is voided in the customer's wallet. Use after a refund or when a single customer's pass should stop working.
- instanceId or barcodeContent (one required)
- reason (optional) — recorded for your audit trail, e.g. "refund"
{
"barcodeContent": "SAVE-8H2K",
"reason": "refund"
}{
"ok": true,
"instanceId": "…"
}Already-revoked instances return ok with alreadyRevoked: true.
If multiple live instances share the code (a shared static code), the call returns 409 AMBIGUOUS_BARCODE with a match count — identify one holder via instanceId, or deliberately revoke all of them with /instances/revoke-all-by-barcode.
/instances/revoke-all-by-barcodeThe deliberate mass revoke: voids every instance carrying a given code. Built for leaked promo codes and ended promotions.
- barcodeContent (required)
- reason (optional)
- dryRun (optional) — returns the count that would be revoked without touching anything; check before pulling the trigger
{
"barcodeContent": "SAVE20",
"reason": "code leaked",
"dryRun": true
}{
"ok": true,
"revokedCount": 137
}Example — issue a pass
curl -X POST https://www.passstudio.online/api/v1/passes/PASS_ID/issue \
-H "Authorization: Bearer ps_live_..." \
-H "Content-Type: application/json" \
-d '{
"email": "customer@example.com",
"name": "Jane Doe",
"fields": { "seat": "12A", "showtime": "Oct 1, 7:30 PM" }
}'The customer receives a branded email with wallet buttons; the response returns the created instance and its unique code.
Credits & billing
API calls consume Pass Studio credits exactly like the same actions in the app — an API key never bypasses billing, and never gets charged more than the app would:
- Issuing a pass costs 1 credit — via
/passes/{passId}/issueor an issuance triggered by/orders/process. The balance is checked first: with insufficient credits nothing is created and the call returns an explicit error. Dedupe hits (alreadyExisted: true) are free — retries never double-charge. - Updating a pass for all holders costs 1 credit per push event (
PATCH /passes/{passId}/fields), same as a studio push. With insufficient credits the field update still applies but the push is skipped, and the response says so. - Single-holder actions are free — per-holder field updates, loyalty point awards, holder lookups, and revokes don't consume credits.
- Team wallets are honored — actions on a team-owned pass bill the team's credit balance, not the key owner's personal one.
The full cost matrix — including redemption scans, bulk campaign rates, and plan differences — is on the Credits & Billing page.
Webhooks
Register webhook endpoints in Pass Studio Settings and receive a signed POST whenever something happens to your passes:
| Event | Fires when |
|---|---|
| pass.issued | A customer receives a pass |
| pass.updated | A pass instance is updated |
| pass.redeemed | A pass code is redeemed |
| pass.removed | A customer removes the pass from their wallet |
| holder.merged | Two holder profiles are identified as the same customer and merged |
Endpoints can subscribe to all events or filter by event type and pass. Deliveries are queued with automatic retries. Example payload for pass.issued:
{
"id": "evt_1789344000123_a1b2c3",
"type": "pass.issued",
"created": 1789344000,
"passId": "…",
"data": {
"instanceId": "…",
"platform": "apple",
"passstudioHolderId": "…",
"email": "customer@example.com",
"phone": "+1 555 010 0100",
"shopifyCustomerId": "8237...",
"barcodeContent": "SAVE-8H2K",
"shareToken": "…"
}
}Every instance-scoped event's data is enriched with the holder's identity — email, phone, and external IDs (e.g. Shopify customer ID) where known, null otherwise — so downstream systems can match the customer without extra lookups.
Verifying signatures
Each delivery carries identifying headers and an HMAC signature:
X-PassStudio-Event: pass.issued
X-PassStudio-Delivery: <delivery id>
X-PassStudio-Timestamp: <unix seconds>
X-PassStudio-Signature: sha256=<hex>Compute HMAC-SHA256(secret, timestamp + "." + rawBody) with your endpoint's signing secret and compare it to the signature header. Reject stale timestamps to prevent replay.
Scenarios
- Sync wallet holders to your CRM. A
pass.issuedwebhook creates or updates the contact in your CRM and tags them "wallet holder" — every pass save becomes a contact with a marketing channel attached. - Loyalty on any platform. Selling outside Shopify? Send each order to
POST /orders/processand get the same automatic loyalty accrual, reward progression, and pass updates — no replatforming. - Win-back automation. A
pass.removedwebhook triggers a win-back flow in your email platform. A customer deleting your pass is a churn signal no other channel gives you. - VIP and milestone rewards. A deal won in your CRM or a spend threshold reached calls issue with per-customer fields (tier, expiry) — a personalized VIP pass lands in the customer's wallet.
- Event ticketing from one template. A form or spreadsheet submission calls issue with seat and showtime fields — each attendee gets a personalized ticket from one evergreen template.
- Incident response for promotions. A single-use code leaks —
POST /instances/revoke-all-by-barcode(dry-run first) kills it across every wallet at once. - In-store recognition. Staff scans the pass in the POS app (or your own system calls
GET /holders/find), awards points, and the pass updates in the customer's hand.
Questions?
Have an integration you need? Tell us — the fastest way to shape the roadmap is to ask: info@idardirect.com.