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.
Email delivery, built in
Any issuance that knows the customer's email — the API, Shopify orders, the Studio's enrollment tools — sends a branded wallet email with the official Add to Apple Wallet / Add to Google Wallet buttons, localized to the customer's language and sent under your sender name. No email tool to wire up. Every API response reports emailSent, and "sendEmail": false hands you the link to deliver yourself.
CSV member import (no code)
Bringing an existing list? The Studio's Holders panel imports memberId, name, email, phone rows — up to 500 per batch — previews the credit cost, emails the wallet link to every row with an address, and reports rows whose email already holds an active card as "already enrolled" instead of minting them twice. One-way import, same cards and same analytics as the API.
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.
Square
Connect your Square account and the register itself becomes the scan surface — no extra hardware, no app at the point of sale:
- Coupons at the register — the cashier scans the customer's pass barcode like a product and the percentage discount applies to the sale instantly, on-device. Works with any barcode scanner, Square Register, or the camera scan in the free Square for Retail app.
- Loyalty at the register — earning scans accrue points with zero cashier steps; when a reward unlocks, the pass flips on the customer's phone and the reward discount applies automatically on their next scan.
- QR enrollment — customers join a loyalty program by adding the welcome pass from a QR code or ad link. No email, no signup form, no online store required — a Square-only business runs the whole loop.
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 non-2xx status with { "error": "…", "code": "…" } — the code is a stable machine-readable contract; branch on it, never on the message. Full table: API error codes. Fair-use request limits: Rate limits.
Your first pass in three calls
Everything else in this reference is optional. A card in a customer's wallet, kept up to date, and taken away again — that's three requests.
1. Issue — creates the card for one customer and emails them the Add-to-Wallet buttons. Idempotent per email: call it twice, they get one card.
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": "alex@example.com", "fields": { "memberName": "Alex Rivera" } }'{
"instanceId": "…",
"barcodeContent": "…",
"addToWalletUrl": "https://www.thepassstudio.com/i/…",
"emailSent": true,
"alreadyExisted": false
}Don't want us to email? Add "sendEmail": false and send addToWalletUrl yourself — same card, your channel. emailSent is true only when a mail actually went out: a new card with an email, or an existing one called with "resendIfExists": true. A plain dedupe hit (alreadyExisted: true) never re-sends.
2. Update — change what's on the card; it refreshes on the phone with a notification.
curl -X PATCH https://www.passstudio.online/api/v1/instances/fields \
-H "Authorization: Bearer ps_live_..." \
-H "Content-Type: application/json" \
-d '{ "instanceId": "…", "fields": { "tier": "Gold" }, "message": "You are Gold now, Alex." }'3. Revoke — the card goes grey in Apple Wallet and is removed from Google Wallet.
curl -X POST https://www.passstudio.online/api/v1/instances/revoke \
-H "Authorization: Bearer ps_live_..." \
-H "Content-Type: application/json" \
-d '{ "instanceId": "…", "reason": "membership ended" }'Apple and Google from the same three calls; we hold the certificates. When you want the card to do things — earn points on orders, flip to a reward, scan at a Square register — that's the rest of this page.
Tracing a call end to end
Send an X-Request-Id header with any API request — your own log id, up to 64 characters (letters, digits, . _ -). We record it alongside our own id and echo it back in the X-Request-Id response header, so a failure in your logs can be matched to ours exactly. If you don't send one, we generate it. Error responses also include requestId in the JSON body — include it when you contact support and we can find the exact call.
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 or the barcode 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 |
| POST | /redeem | Redeem a pass code at your counter (POS scan) |
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",
"billing": { "credits": 480, "plan": "free" }
}billing reports the key owner's spendable credits and effective plan (free | growth | pro) — the same numbers the billing page shows, resolved the same way the billing gates resolve them. Poll it to top up before a push is skipped (202) or a call is blocked (402).
Team keys add billing.billedThrough: true when the workspace bills through a parent account — credits and plan then reflect the paying account.
/passesLists the account's passes, including each pass's editable field keys — use it to discover valid keys before calling the field-update endpoints.
- Optional pagination: limit (1–200) and cursor (a prior page's nextCursor). Without limit the full list returns.
- Optional locale (e.g. fr) — returns fieldLabels in that language; unsupported tags fall back to English.
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"],
"fieldLabels": { "expiry": "Expiry Date", "tier": "Tier" },
"templateOwnedFieldKeys": []
}]
}Custom Passes list each design field's stable row key (e.g. field_1787677019449) with the label from the editor — keys never change on a label rename, so integrations built on them don't break. Custom field values are editable through both update endpoints: per holder via PATCH /instances/fields, or for every holder at once via PATCH /passes/{passId}/fields.
templateOwnedFieldKeys lists keys managed by the pass template — they can only be updated for all holders at once, never per holder.
With limit set, the response adds nextCursor and hasMore — pass nextCursor as cursor to fetch the next page. The no-limit full list is kept for compatibility; new integrations should paginate.
/passes/{passId}/issueIssues a pass. All identifiers are optional — email, phone, passstudioHolderId, customerId, or none at all (an anonymous card). Mints a unique code per the pass's barcode settings, stamps contact details, and — when the call carries an email — sends a branded email with Apple / Google Wallet buttons.
- Identifiers (each optional; when present they attach identity and dedupe the issuance — probe order holderId → email → phone → customerId — so a retried call returns the existing card):
- email — the customer's email; the only identifier that triggers the wallet email
- phone — the customer's phone; matched with the same reversed-suffix semantics as Find Holder (formatting and country-code variants collide); must be plausible (≥7 real digits) when it is the only identifier; nothing is emailed — deliver addToWalletUrl through your own channel (SMS-first businesses live here)
- passstudioHolderId — Pass Studio's holder id (from Find Holder); the new card links directly to that holder; a merged-away id resolves to its successor automatically; unknown ids return 404 holder_not_found
- customerId (+ source, default "api") — your system's customer id, stored source-scoped as "<source>:<customerId>" (same rules as Process Order: no ":", "shopify" reserved); stamped on the card as a stable handle and usable as a dedupe key on later calls
- No identifier at all — a card is still minted (anonymous); each such call mints a fresh card
- name (optional) — stamped onto the holder profile
- sendEmail (optional, default true) — applies only when email is present; set false to create the instance without emailing
- utmSource, utmMedium, utmCampaign (optional) — first-touch attribution, stored verbatim on the card and carried on every event it ever emits (BigQuery + Segment), all the way to redemption. Defaults when unset: utmSource "pass-studio_api", utmMedium "issue". First-touch: a dedupe hit keeps the card's original stamps
- locale (optional) — language of the wallet email ("fr", "pt-BR"); unsupported tags fall back to the pass's own language
- resendIfExists (optional) — re-send the wallet email to an existing holder. Without it, a dedupe hit (alreadyExisted: true) returns emailSent: false — the card already exists and nothing is re-sent
- fields (optional) — per-issuance field values, e.g. seat or showtime; keys must exist on the pass template, and template-owned keys are rejected
- includeArtifacts (optional) — set true to add an artifacts object with the raw per-platform delivery URLs (see note below); default false keeps the response unchanged
{
"email": "customer@example.com",
"name": "Jane Doe",
"phone": "+1 555 010 0100",
"sendEmail": true,
"fields": { "seat": "12A", "showtime": "Oct 1, 7:30 PM" },
"includeArtifacts": true
}{
"instanceId": "…",
"passstudioHolderId": null,
"barcodeContent": "SAVE-8H2K",
"shareToken": "…",
"addToWalletUrl": "https://www.passstudio.online/i/…",
"shareUrl": "https://www.passstudio.online/share/…",
"platform": "apple",
"emailSent": true,
"alreadyExisted": false,
"artifacts": {
"appleWalletUrl": "https://www.passstudio.online/api/i/…/download?platform=apple&raw=1",
"googleWalletUrl": "https://www.passstudio.online/api/i/…/download?platform=google&redirect=1&raw=1"
}
}Idempotent: re-issuing to the same identifier (holderId, email, phone, or customerId — or the same barcodeValue) 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.
Status codes: a new issuance returns 201 Created; a dedupe hit returns 200 with the existing instance. Treat any 2xx as success — branch on alreadyExisted, not the status code.
Each new issuance costs 1 credit.
Delivery is explicit: the wallet email goes out only when the call itself carries email. Otherwise the response's addToWalletUrl is yours to deliver (SMS, your own email, a page).
passstudioHolderId in the response is the current holder id ONLY when a strong identity already existed for the email, phone, or passstudioHolderId you passed — null otherwise (including the first time an email/phone is seen: holder identity is established at register time, not minted at issue time). The durable per-card keys to store are instanceId and barcodeContent.
artifacts (with includeArtifacts: true) are the raw per-platform delivery URLs for building your own "Add to Apple Wallet" / "Add to Google Wallet" buttons — in your own emails, pages, or messages. appleWalletUrl serves the .pkpass file directly (on iPhone the add sheet opens immediately); googleWalletUrl redirects straight into Google's save flow. Long-lived and unguessable, no HTML in between — the raw=1 parameter guarantees the artifact from ANY client, including server-side fetchers (python-requests, curl, axios) for automated attachment pipelines. First-touch attribution from issuance is carried on every event regardless, and you can append your own utm_* parameters per placement (e.g. &utm_medium=email) — each click records them. Dedupe hits return the same instance's URLs.
/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.
No-change calls are free: when every value you send equals the template's current value (and no locations ride the call), nothing is written or pushed — 200 with pushed: false, pushSkipReason: "no_change", and no charge. Re-sending desired state is always safe.
Custom Passes: send each field's stable row key from GET /passes. The update changes the field's value on the template — a holder who has an individual value for a key keeps it (per-holder values override the template), so for a value that should read the same on every card, update it only here and leave that key out of your per-holder calls.
/instances/fieldsUpdates a single holder's pass — field values, the barcode, or both — and pushes the change to their wallet immediately, optionally with a notification.
- instanceId or barcodeContent (one required) — identifies the holder's instance
- fields — { "key": "value" } map of per-holder values; at least one of fields | barcodeValue | message is required. A null value CLEARS the holder's override for that key — the card goes back to the template value (response lists it under clearedFields; keys that were never personalized clear as a warned no-op)
- barcodeValue (optional) — replaces the string rendered in the pass's barcode: the same externally-generated code the issue endpoint accepts, opaque to Pass Studio
- message (optional) — a notification shown to the holder with the update (Apple lock-screen banner, Google Wallet notification): a plain string, or per-language { "en": "…", "fr": "…" } — the holder's device picks its language
- externalRef (optional) — idempotency key: a repeated ref returns the original result with alreadyProcessed: true, with no second push and no second notification — a retried automation never re-banners the holder. Same convention as Award Points.
- Template-owned keys are rejected (update those for all holders via PATCH /passes/{passId}/fields); unknown keys are rejected with the valid-key list
- Custom Passes: every editor field's VALUE is per-holder editable, addressed by its stable row key from GET /passes (fieldLabels maps keys to your labels). Labels, positions, and the field set stay on the template.
{
"instanceId": "inst_abc",
"fields": {
"memberName": "Jon Appleseed",
"memberId": "182746",
"tier": "Gold",
"points": "750"
},
"barcodeValue": "PS_JH765GF",
"message": {
"en": "Welcome, Gold member!",
"fr": "Bienvenue, membre Gold !"
}
}{
"ok": true,
"instanceId": "inst_abc",
"updatedFields": ["memberName", "memberId", "tier", "points"],
"barcodeContent": "PS_JH765GF",
"previousBarcodeContent": "…",
"pushed": true
}One push covers fields, barcode, and message together — 1 credit per call (an individual push, like Save & push in the studio), charged only when the push actually fires. Fields-only calls with insufficient credits still apply the update but skip the push — the response is a 202 with pushed: false, pushSkipReason: "insufficient_credits", and a warnings entry, so the skip stands out in your logs; top up and re-send to deliver. A message-only call or a call carrying barcodeValue is all-or-nothing — the notification (or the barcode push) is the whole point, so with insufficient credits nothing changes (402).
Re-issuing the same holder must reuse the originally issued barcodeValue — issue-call idempotency keys on it, not on later values. Scans at Pass Studio registers always resolve the current value.
A barcode change requires a push to ensure pass integrity — the stored code and the code the holder's wallet displays must change together, or scans fail. With insufficient credits a barcode change therefore fails outright (402, nothing changes); it is never saved without being delivered.
A replayed externalRef returns the stored response verbatim — including pushed: false if the original call skipped the push. To re-attempt a skipped push, call again with a new ref.
No-change calls are free: re-sending values the holder's card already carries (with no barcode change and no message) is detected and skipped — 200 with pushed: false, pushSkipReason: "no_change", and no charge, so CRM syncs can re-send desired state without change detection of their own. Two deliberate exceptions still push and bill: a message always delivers (a notification is an event, not state), and setting a value on a field the holder had never personalized is a real change even when it looks identical — it pins that value for the holder against future template-wide updates.
Cards added before per-holder addressing existed (shared Simple-mode cards) return 409 fields_update_unsupported — several wallets share one card record, so a per-holder value would render on all of them. This is about the card's own history, not the pass's current distribution mode: switching the pass to Unique does not make old shared cards addressable; the holder re-adding the pass does. Rarer: a holder who saved to GOOGLE Wallet before per-holder addressing gets the update with a 200 plus a warnings entry — their Google card may not reflect it until they re-add (Apple updates normally).
/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, customerName (optional) — customer identity
- customerId (optional) — your system's customer id. Learned on the first order matched by code, email, or phone; from then on it works as the sole identifier. Scoped per source, so ids from different systems never collide. An order identified by customerId alone (no discount code) must also carry passId — without it the order returns no_pass_configured before matching runs.
- discountCodes (optional) — codes applied to the order; matching precedence is discount code → customerId → 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 (required) — becomes the audit trail for every revoked pass
- 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
}/redeemRedeems a pass code at your counter — the endpoint behind the POS staff app. Validates the code, decrements uses, pushes the updated pass to the customer's wallet, fires pass.redeemed, and advances a journey when the pass is spent.
- code (required) — the scanned barcode content
- posTransactionId (optional) — your register's transaction reference, echoed on the webhook
- posId (optional) — identifies the register or terminal for scan-window billing on Growth/Pro; omit it and all API scans share one register bucket
- Lives under the /v1 base like every other endpoint. The original /api/redeem path (pre-/v1) still works during the deprecation period — its responses carry a DEPRECATED warning; please migrate to /api/v1/redeem.
curl -X POST https://www.passstudio.online/api/v1/redeem \
-H "Authorization: Bearer ps_live_..." \
-H "Content-Type: application/json" \
-d '{ "code": "1036GHD" }'{
"valid": true,
"instanceId": "…",
"passId": "…",
"barcodeContent": "1036GHD",
"offerTitle": "10% off",
"holderName": "Jane Doe",
"expiresAt": null,
"remainingUses": 0,
"message": "Redeemed"
}Invalid codes return { valid: false } with a reason: already_redeemed, uses_exhausted, expired, pass_inactive, or not_found — render the message to the cashier.
Redemption scans bill like in-app scans: pay-as-you-go pays per scan; subscription plans meter per 15-minute window per register per pass (pass posId to identify the register).
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. - Updating a pass for one holder costs 1 credit per push event (
PATCH /instances/fields), like a studio push — with insufficient credits a fields update still applies but the push is skipped (202). A message-only or barcode-changing call is the exception: delivery is the whole point of those, so they need balance up front (402, nothing changes). Loyalty point awards, holder lookups, and revokes are free. - 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.
Idempotency
Every mutating endpoint is safe to retry. Where a key is involved, a repeated call returns the original result instead of applying twice:
| Endpoint | Key | Repeat returns |
|---|---|---|
| POST /passes/{passId}/issue | holderId → email → phone → customerId (or external barcodeValue) | the existing instance, alreadyExisted: true |
| PATCH /instances/fields | externalRef (optional — without it, last write wins) | stored result, alreadyProcessed: true — no second push or notification. Re-sending the current barcodeValue is never an error: it's skipped with a response warning while any changed fields or message still apply; an empty fields object is likewise ignored with a warning (a message-only call still delivers), and a fully change-free call — including one that re-sends the exact values the card already carries — returns 200 with pushSkipReason: "no_change" and no credit charge. A push skipped for lack of credits returns 202 (and its replay stays 202) — the ref is spent, so after topping up re-send with a fresh ref to deliver. A message-only call at zero balance is a 402 before the ref is claimed — retry with the SAME ref after topping up |
| POST /loyalty/points | externalRef (optional) | stored result, alreadyProcessed: true |
| POST /orders/process | source + externalOrderId | original result, alreadyProcessed: true |
| POST /instances/revoke | — (naturally idempotent) | alreadyRevoked: true |
Compose externalRef as <source>_<externalOrderId> when you want a points award to collide with an order sent to Process Order. A 500 with retryable: true releases the claim — the retry re-attempts rather than replaying.
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. On Square, the loop is native — scans at the register enroll, accrue, and redeem. Selling anywhere else? 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.
- Migrate a member list in an afternoon. Export members from the old system, paste them into the Studio's CSV import (500 per batch), and every member with an email gets the wallet link that day — no code, no double-mints for people who already enrolled.
- 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: support@thepassstudio.com.