PassMaker API
Generate Apple & Google Wallet passes from your templates, programmatically. REST, JSON, and the same credits and delivery pipeline as the web app.
Overview
The API executes the templates you design in PassMaker. Each pass costs 1 credit (5 for hybrid), deducted exactly like the web app. Base URL:
https://passmaker.io/api/v1
Design a template in the dashboard, mark fields as variables, then call the API with values for those variables.
Authentication
Create an API key in Dashboard → API Keys. Keys look like pm_live_… and are shown once. Send it as a bearer token on every request:
Authorization: Bearer pm_live_your_key_here
Keys carry scopes (passes:write, passes:read, templates:read, batches:write). A request missing a required scope returns 403.
Quickstart
- Create an API key (Dashboard → API Keys).
- Find a template ID and its variables — the template detail page has a “Use via API” snippet, or call
GET /templates. - Create a pass:
curl https://passmaker.io/api/v1/passes \
-H "Authorization: Bearer pm_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"template_id": "your-template-uuid",
"variables": { "name": "Ada Lovelace", "seat": "12B" },
"recipient": { "email": "ada@example.com" },
"delivery": "email"
}'The 201 response returns the pass id, an Apple download_url, a Google save_url (if the template targets Google), and — for hybrid templates — a hybrid_view_url. Use "delivery": "none" to skip email and deliver the links yourself.
Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /templates | List active templates |
| GET | /templates/{id} | Template detail + variable schema |
| POST | /passes | Create a pass from a template |
| GET | /passes | List passes (paginated) |
| GET | /passes/{id} | Pass status + artifacts |
| POST | /batches | Create up to 500 passes (async) |
| GET | /batches | List batches |
| GET | /batches/{id} | Batch status + per-item results |
| POST | /webhooks | Subscribe to events (returns secret once) |
| GET | /webhooks | List webhook endpoints |
| POST | /webhooks/{id}/test | Send a sample event (test trigger) |
| GET | /webhooks/events | Event types + sample payloads |
| DELETE | /webhooks/{id} | Unsubscribe |
| GET | /account | Credits, scopes, limits |
Errors
Errors use a consistent envelope with an appropriate HTTP status:
{ "error": { "code": "insufficient_credits", "message": "This pass costs 1 credit." } }Common codes: unauthorized (401), insufficient_credits (402), insufficient_scope (403), not_found (404), idempotency_key_reused (409), invalid_variables (422), rate_limit_exceeded (429). A 422 includes a variables array naming each missing field.
Idempotency
Write endpoints accept an Idempotency-Key header. Retrying with the same key and body returns the original response (marked Idempotent-Replayed: true) and spends no extra credits. The same key with a different body returns 409.
curl https://passmaker.io/api/v1/passes \
-H "Authorization: Bearer pm_live_..." \
-H "Idempotency-Key: order-4815-pass" \
-H "Content-Type: application/json" \
-d '{ "template_id": "...", "variables": { "name": "Ada" } }'Rate limits
Requests are rate-limited per key (default 60/min). Every response includes:
X-RateLimit-Limit: 60 X-RateLimit-Remaining: 58 X-RateLimit-Reset: 1753650000
A 429 response includes Retry-After (seconds). Credits are the underlying economic limit beneath the rate limit.
Webhooks
Subscribe to events in Dashboard → Webhooks, or programmatically via the /webhooks API (webhooks:write scope) — this is how Zapier & Make register triggers. Events: pass.created, pass.email_delivered, pass.email_bounced, batch.completed, batch.failed. PassMaker POSTs a signed JSON body and retries non-2xx responses with backoff.
Testing your endpoint? POST /webhooks/{id}/test sends a sample event on demand, and GET /webhooks/events returns every event type with a sample payload — so you can build and map fields before a real event fires.
Verify the X-PassMaker-Signature: t=<unix>,v1=<hmac> header — HMAC-SHA256 over `${t}.${rawBody}` using your endpoint secret:
const crypto = require('crypto')
function verify(rawBody, header, secret) {
const [, t, v1] = header.match(/t=(\d+),v1=([a-f0-9]+)/)
const expected = crypto.createHmac('sha256', secret)
.update(t + '.' + rawBody).digest('hex')
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
}