← Documentation

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

MethodEndpointPurpose
GET/meVerify your key and identify the account
GET/passesList your passes with their editable field keys
POST/passes/{passId}/issueIssue a pass to a customer (idempotent per email)
PATCH/passes/{passId}/fieldsUpdate field values for all holders of a pass
PATCH/instances/fieldsUpdate field values for one holder, with instant push
GET/holders/findFind a holder by email, phone, or pass code
POST/loyalty/pointsAward or adjust loyalty points
POST/orders/processFeed an order into the loyalty engine
POST/instances/revokeRevoke a single pass instance
POST/instances/revoke-all-by-barcodeRevoke every instance carrying a given code

Endpoint reference

GET/me

Verifies the API key and returns the account it belongs to. Use it as a connection test.

Request
  • No parameters.
Example request
curl https://www.passstudio.online/api/v1/me \
  -H "Authorization: Bearer ps_live_..."
Response
{
  "id": "account id",
  "email": "you@example.com",
  "displayName": "Your Name",
  "keyLabel": "My integration key"
}
GET/passes

Lists the account's passes, including each pass's editable field keys — use it to discover valid keys before calling the field-update endpoints.

Request
  • No parameters.
Example request
curl https://www.passstudio.online/api/v1/passes \
  -H "Authorization: Bearer ps_live_..."
Response
{
  "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.

POST/passes/{passId}/issue

Issues 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.

Request
  • 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
Request body
{
  "email": "customer@example.com",
  "name": "Jane Doe",
  "phone": "+1 555 010 0100",
  "sendEmail": true,
  "fields": { "seat": "12A", "showtime": "Oct 1, 7:30 PM" }
}
Response
{
  "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.

PATCH/passes/{passId}/fields

Updates 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.

Request
  • 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
Request body
{
  "fields": { "expiry": "Dec 31, 2026" },
  "locations": [
    { "latitude": 40.7128, "longitude": -74.006, "relevantText": "Welcome back!" }
  ]
}
Response
{
  "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.

PATCH/instances/fields

Updates field values for a single holder — their personal copy of the pass — and pushes the change to their wallet immediately.

Request
  • 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
Request body
{
  "barcodeContent": "SAVE-8H2K",
  "fields": { "seat": "14C" }
}
Response
{
  "ok": true,
  "instanceId": "…",
  "updatedFields": ["seat"],
  "pushed": true
}
GET/holders/find

Looks up a customer and returns their pass instances, most-recently-active first, including loyalty state where applicable.

Request
  • Exactly one of: ?email= | ?phone= | ?barcodeContent=
  • ?passId= (optional) — scope the search to one pass
Example request
curl "https://www.passstudio.online/api/v1/holders/find?email=customer@example.com" \
  -H "Authorization: Bearer ps_live_..."
Response
{
  "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.

POST/loyalty/points

Awards 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.

Request
  • 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
Request body
{
  "delta": 25,
  "email": "customer@example.com",
  "passId": "…",
  "externalRef": "pos_receipt_48211"
}
Response
{
  "newBalance": 105,
  "rewardUnlocked": true,
  "newBarcodeContent": "RWD-93KF",
  "alreadyProcessed": false
}
POST/orders/process

Feeds 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.

Request
  • 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
Request body
{
  "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" }
  ]
}
Response
{
  "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.

POST/instances/revoke

Revokes 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.

Request
  • instanceId or barcodeContent (one required)
  • reason (optional) — recorded for your audit trail, e.g. "refund"
Request body
{
  "barcodeContent": "SAVE-8H2K",
  "reason": "refund"
}
Response
{
  "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.

POST/instances/revoke-all-by-barcode

The deliberate mass revoke: voids every instance carrying a given code. Built for leaked promo codes and ended promotions.

Request
  • barcodeContent (required)
  • reason (optional)
  • dryRun (optional) — returns the count that would be revoked without touching anything; check before pulling the trigger
Request body
{
  "barcodeContent": "SAVE20",
  "reason": "code leaked",
  "dryRun": true
}
Response
{
  "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}/issue or 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:

EventFires when
pass.issuedA customer receives a pass
pass.updatedA pass instance is updated
pass.redeemedA pass code is redeemed
pass.removedA customer removes the pass from their wallet
holder.mergedTwo 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.issued webhook 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/process and get the same automatic loyalty accrual, reward progression, and pass updates — no replatforming.
  • Win-back automation. A pass.removed webhook 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.