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

  1. Create an API key (Dashboard → API Keys).
  2. Find a template ID and its variables — the template detail page has a “Use via API” snippet, or call GET /templates.
  3. 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

MethodPathDescription
GET/templatesList active templates
GET/templates/{id}Template detail + variable schema
POST/passesCreate a pass from a template
GET/passesList passes (paginated)
GET/passes/{id}Pass status + artifacts
POST/batchesCreate up to 500 passes (async)
GET/batchesList batches
GET/batches/{id}Batch status + per-item results
POST/webhooksSubscribe to events (returns secret once)
GET/webhooksList webhook endpoints
POST/webhooks/{id}/testSend a sample event (test trigger)
GET/webhooks/eventsEvent types + sample payloads
DELETE/webhooks/{id}Unsubscribe
GET/accountCredits, 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))
}