API Reference
REST API for creating and managing Apple Wallet and Google Wallet passes.
Base URL
https://api.walletpassapi.com
How it works
- Design your pass template in the portal — set layout, colours, images, and define
{variable}placeholders. - Copy the template ID from the portal (or use GET /v1/templates).
- Call POST /v1/issue with the template ID and variable values to issue a personalised pass to each recipient.
- Update passes anytime with PUT /v1/passes/:id — the change is pushed live to installed devices.
All requests and responses use JSON. Include Content-Type: application/json on requests with a body.
Authentication
The API uses two types of Bearer keys:
wk_…
Workspace key — pass operations
Used for all pass endpoints (POST /v1/issue, GET /v1/passes, etc.) and template lookups. Scopes all activity to a specific workspace.
Authorization: Bearer wk_xxxxxxxxxxxxxxxxxxxxxxxx
sk_…
Account key — account & workspace management
Used for account management (/v1/accounts/me) and workspace management (/v1/workspaces). Generate your key from Settings → Account API Key in the portal.
Authorization: Bearer sk_xxxxxxxxxxxxxxxxxxxxxxxx
Keep both keys secret — anyone holding them can manage your workspaces and passes.
Errors
All errors return a consistent JSON body with a SCREAMING_SNAKE_CASE code, a human-readable message, and a unique request ID:
{
"error": {
"code": "INVALID_REQUEST",
"message": "template_id is required",
"request_id": "req_01j9abc..."
}
}
| Status | Code | Meaning |
|---|---|---|
| 400 | INVALID_REQUEST | Missing or malformed request |
| 400 | INVALID_OVERRIDE | Override object contained a key other than content |
| 400 | RESERVED_VARIABLE_NAME | A system variable name was used in data |
| 400 | MODULE_OWNED_VARIABLE | Variable is managed by the loyalty/coupon module and cannot be set directly |
| 401 | UNAUTHORIZED | Missing or invalid API key |
| 402 | OPERATION_LIMIT | Workspace operation limit reached |
| 404 | NOT_FOUND | Resource does not exist |
| 404 | PASS_NOT_FOUND | Pass does not exist |
| 404 | TEMPLATE_NOT_FOUND | Template does not exist |
| 409 | IDEMPOTENCY_KEY_CONFLICT | Same idempotency key used with different template or recipient |
| 422 | MISSING_PLACEHOLDER_VALUE | Template has missing_placeholder_behavior: "error" and a placeholder had no value |
| 422 | INVALID_ASSET | Asset upload failed validation (wrong mime type or exceeds 500 KB) |
| 422 | INSUFFICIENT_POINTS | Loyalty point deduction would result in negative balance |
| 422 | COUPON_NOT_YET_VALID | Coupon scheme valid_from is in the future |
| 422 | SCHEME_EXPIRED | Coupon scheme has passed its expiry date |
| 422 | REDEMPTION_LIMIT_REACHED | Pass has reached the scheme's per-pass redemption limit |
| 429 | RATE_LIMITED | Too many requests — back off and retry |
| 500 | INTERNAL_ERROR | Unexpected server error |
Rate limits
Requests are rate-limited per API key. When exceeded the API returns 429 rate_limited. Implement exponential backoff and retry after a short delay.
Accounts
Account management uses your sk_… key. The /me path always refers to the account that owns the key — no ID required in the URL.
/v1/accounts
No auth required
Create a new account and a default workspace. The api_key (sk_) and workspace_id are returned only once — store them securely.
Request body
| Field | Description |
|---|---|
namerequired | Display name for the account |
emailrequired | Contact email address (must be unique) |
webhook_urloptional | HTTPS URL to receive webhook events |
Response 201
{
"id": "acc_01j9...",
"name": "Acme Corp",
"email": "hello@acme.com",
"workspace_id": "ws_01j9...",
"api_key": "sk_xxxxxxxxxxxxxxxxxxxxxxxx",
"webhook_secret": "whsec_xxxxxxxxxxxxxxxx",
"portal_url": "/portal",
"created_at": "2025-01-15T10:00:00.000Z"
}
api_key and webhook_secret are shown only on creation.
/v1/accounts/me
Retrieve your account details.
Response 200
{
"id": "acc_01j9...",
"name": "Acme Corp",
"email": "hello@acme.com",
"plan": "starter",
"trial_ends_at": null,
"api_key_prefix": "sk_a1b2c3d4",
"webhook_url": "https://example.com/hooks",
"created_at": "2025-01-15T10:00:00.000Z",
"updated_at": "2025-01-15T10:00:00.000Z"
}
/v1/accounts/me
Update account settings. Only provided fields are changed.
Request body
| Field | Description |
|---|---|
nameoptional | New display name |
webhook_urloptional | New webhook URL (pass null to remove) |
rotate_webhook_secretoptional | Pass true to regenerate the webhook signing secret — new secret returned in response |
Response 200
{ "id": "acc_01j9...", "name": "Acme Corp", "webhook_url": "...", "updated_at": "..." }
When rotate_webhook_secret: true, the response also includes webhook_secret — store it immediately as it is not shown again.
/v1/accounts/me
Permanently delete the account and all associated workspaces, passes, and device registrations. This cannot be undone.
Response 200
{
"deleted": true,
"deleted_passes": 42,
"deleted_registrations": 87
}
/v1/accounts/me/rotate-key
Invalidate the current sk_ key and issue a new one. The old key stops working immediately. The new key is shown only once.
Response 200
{
"api_key": "sk_yyyyyyyyyyyyyyyyyyyyyyyy",
"message": "API key rotated. Store the new key — it will not be shown again."
}
/v1/accounts/me/credentials
Upload Apple Wallet and/or Google Wallet signing credentials. All binary or multi-line values must be base64-encoded. Only the fields you provide are updated — existing credentials are preserved. Account-level credentials are used by all workspaces that have Use account credentials enabled.
Apple Wallet fields
| Field | Description |
|---|---|
apple_p12_cert | Pass signing certificate (.p12 file), base64-encoded |
apple_p12_password | Password protecting the .p12 file |
apple_pass_type_identifier | Pass Type ID, e.g. pass.com.example.mypass |
apple_team_id | 10-character Apple Developer Team ID |
apple_apns_private_key | APNs auth key (.p8 contents), base64-encoded |
apple_apns_key_id | 10-character APNs Key ID |
Google Wallet fields
| Field | Description |
|---|---|
google_issuer_id | Issuer ID from Google Pay & Wallet Console |
google_service_account_email | Service account email address |
google_service_account_key | Service account JSON key file, base64-encoded |
Response 200
{
"updated": ["apple_p12_cert", "apple_pass_type_identifier", "apple_team_id"]
}
Workspaces
Workspace management requires an account sk_… key. Each workspace has its own wk_… key for pass operations and its own operation budget.
/v1/workspaces
List all workspaces for the account, along with the account's operation pool summary.
Response 200
{
"workspaces": [
{
"id": "ws_01j9...",
"name": "Production",
"is_default": true,
"operations_allocated": 5000,
"operations_used": 120,
"api_key_prefix": "wk_a1b2c3d4",
"use_account_credentials": true,
"created_at": "2025-01-15T10:00:00.000Z"
}
],
"total_ops": 10000,
"allocated": 5000,
"unallocated": 5000
}
/v1/workspaces
Create a new workspace. Returns the workspace wk_ API key once — save it immediately.
Request body
| Field | Description |
|---|---|
namerequired | Workspace display name |
operations_allocatedoptional | Operations to allocate from the account pool (default: 0) |
Response 201
{
"workspace": {
"id": "ws_01j9...",
"name": "Staging",
"is_default": false,
"operations_allocated": 1000,
"operations_used": 0,
"api_key_prefix": "wk_a1b2c3d4",
"use_account_credentials": true,
"created_at": "2025-06-01T12:00:00.000Z"
},
"api_key": "wk_xxxxxxxxxxxxxxxxxxxxxxxx"
}
/v1/workspaces/:id
Retrieve a single workspace.
Response 200
{ "workspace": { "id": "ws_01j9...", "name": "Production", ... } }
/v1/workspaces/:id
Update workspace name or operation allocation. Only provided fields are changed.
Request body
| Field | Description |
|---|---|
nameoptional | New display name |
operations_allocatedoptional | New operation allocation (must not exceed unallocated pool) |
Response 200
{ "workspace": { "id": "ws_01j9...", "name": "Production (updated)", ... } }
/v1/workspaces/:id
Delete a workspace and all its passes. The default workspace cannot be deleted.
Response 200
{ "ok": true, "deleted_passes": 15 }
/v1/workspaces/:id/rotate-key
Invalidate the workspace's current wk_ key and issue a new one. The old key stops working immediately.
Response 200
{
"api_key": "wk_yyyyyyyyyyyyyyyyyyyyyyyy",
"message": "Save the new api_key — it will not be shown again"
}
Templates
Templates are designed in the portal. These endpoints let you list templates and look up IDs and variable schemas programmatically. Requires a wk_… workspace key.
/v1/templates
List all pass templates in the workspace.
Response 200
{
"data": [
{
"id": "tmpl_a1b2c3d4",
"name": "Loyalty Card",
"pass_style": "storeCard",
"variable_schema": [
{ "name": "member_name", "type": "text", "required": true },
{ "name": "points", "type": "text", "required": false },
{ "name": "tier", "type": "text", "required": false }
],
"created_at": "2025-01-15T10:00:00.000Z",
"updated_at": "2025-06-01T09:00:00.000Z",
"last_pushed_at": "2025-06-01T09:00:00.000Z"
}
],
"total": 1
}
/v1/templates/:id
Retrieve a single template including its full variable schema.
Response 200
{
"id": "tmpl_a1b2c3d4",
"name": "Loyalty Card",
"pass_style": "storeCard",
"variable_schema": [
{ "name": "member_name", "type": "text", "required": true },
{ "name": "points", "type": "text", "required": false }
],
"created_at": "2025-01-15T10:00:00.000Z",
"updated_at": "2025-06-01T09:00:00.000Z",
"last_pushed_at": null
}
/v1/templates/:id/passes
List all passes issued from a specific template, with full identity and variable fields. Supports pagination.
Query parameters
| Parameter | Description |
|---|---|
limitoptional | Results per page — default 50, max 100 |
offsetoptional | Pagination offset — default 0 |
Response 200
{
"data": [ { "id": "pass_01j9...", "email": "jane@example.com", "variables": { "points": "1,250" }, ... } ],
"total": 847,
"limit": 50,
"offset": 0
}
/v1/templates
Auth: wk_ key
Create a new pass template. Returns the created template.
Request body
| Field | Description |
|---|---|
namerequired | Display name for the template |
pass_dataoptional | Legacy flat pass layout object. Either pass_data or the nested template key is required. |
templateoptional | Spec-canonical nested body — accepts content, appearance, and assets sub-objects (see example below) |
pass_typeoptional | Unified pass type: generic | loyalty | coupon | event — default generic |
platformsoptional | Which wallet platforms to generate — ["apple"], ["google"], or ["apple","google"] (default both) |
missing_placeholder_behavioroptional | error | empty | keep — what to do when a placeholder has no value at issue time (default empty) |
defaultsoptional | Object of default variable values applied before caller-supplied values — alias for defaults_json |
assetsoptional | Map of asset name → { source: "https://..." | "data:image/png;base64,..." }. Server fetches/decodes, validates (PNG/JPEG/GIF/WebP, max 500 KB), stores in R2, and returns metadata in assets_json. Standard names: icon, logo, strip, thumbnail, background, footer, hero |
variable_schemaoptional | Array of variable definitions, e.g. [{ "name": "points", "label": "Points Balance", "type": "string" }] |
module_modeoptional | One of template_only | loyalty_module | coupon_module — default template_only |
loyalty_scheme_idoptional | Link to an existing loyalty scheme |
coupon_scheme_idoptional | Link to an existing coupon scheme |
missing_placeholder_behavior
| Value | Behaviour at issue / update time |
|---|---|
empty (default) | Unresolved {{var}} placeholders are replaced with an empty string |
error | Returns 422 MISSING_PLACEHOLDER_VALUE if any placeholder has no value |
keep | Unresolved placeholders are left as-is in the pass (useful for debugging) |
Example — nested body shape (recommended)
{
"name": "Coffee Club",
"pass_type": "loyalty",
"platforms": ["apple", "google"],
"module_mode": "loyalty_module",
"loyalty_scheme_id": "ls_123",
"template": {
"content": { "title": "{{brand_name}}", "points_label": "Points" },
"appearance": { "background_color": "#4A90D9" },
"assets": {
"icon": { "source": "https://example.com/icon.png" }
}
},
"defaults": { "brand_name": "Acme Coffee" },
"missing_placeholder_behavior": "error"
}
Example — legacy flat shape
{
"name": "Coffee Rewards Card",
"pass_data": {
"pass_style": "storeCard",
"description": "Coffee Rewards",
"... other pass fields": "..."
},
"variable_schema": [
{ "name": "points", "label": "Points Balance", "type": "string" }
],
"module_mode": "template_only"
}
Response 201 Created
{
"id": "tmpl_a1b2c3d4",
"name": "Coffee Rewards Card",
"pass_style": "storeCard",
"placeholder_schema": { ... },
"variable_schema": [
{ "name": "points", "label": "Points Balance", "type": "string" }
],
"module_mode": "template_only",
"created_at": "2025-06-01T12:00:00.000Z",
"updated_at": "2025-06-01T12:00:00.000Z"
}
/v1/templates/:id
Auth: wk_ key
Update an existing template. All fields are optional — only provided fields are updated.
Request body
| Field | Description |
|---|---|
nameoptional | New display name |
pass_dataoptional | Replacement pass layout object (legacy flat shape) |
templateoptional | Nested shape — content, appearance, assets |
pass_typeoptional | generic | loyalty | coupon | event |
platformsoptional | ["apple"] | ["google"] | ["apple","google"] |
missing_placeholder_behavioroptional | error | empty | keep |
defaultsoptional | Default variable values |
assetsoptional | Map of asset name → { source } — see Create for full details |
variable_schemaoptional | Replacement variable schema array |
module_modeoptional | One of template_only | loyalty_module | coupon_module |
Response 200
{
"id": "tmpl_a1b2c3d4",
"name": "Coffee Rewards Card (updated)",
"pass_style": "storeCard",
"placeholder_schema": { ... },
"variable_schema": [ ... ],
"module_mode": "loyalty_module",
"created_at": "2025-06-01T12:00:00.000Z",
"updated_at": "2025-06-16T09:00:00.000Z"
}
/v1/templates/:id
Auth: wk_ key
Delete a template. This does not affect already-issued passes.
Response 200
{
"deleted": true
}
Passes
All pass endpoints use a wk_… workspace key. Passes are issued from templates and personalised with variable values.
/v1/issue
Primary creation path
Issue a personalised pass from a template. If a pass already exists for this email (or external_id), it is updated and pushed to installed devices instead of creating a duplicate — making this endpoint safe to call repeatedly.
Variable values are substituted into any {variable_name} placeholders in the template at issue time.
Request body
| Field | Description |
|---|---|
template_idrequired | ID of the template to issue from (find in the portal or via GET /v1/templates) |
emailrequired | Recipient email address — used as the upsert key |
first_nameoptional | Recipient first name — also available as the {first_name} variable |
last_nameoptional | Recipient last name — also available as {last_name} and {full_name} |
sms_mobileoptional | Mobile number — also available as {sms_mobile}. Alias: sms_number |
external_idoptional | Your own ID for this person — when provided, used as the upsert key instead of email |
tagsoptional | Array of strings for segmentation, e.g. ["vip", "london"] |
variablesoptional | Object of key/value pairs to substitute into template placeholders, e.g. { "points": "1,250", "tier": "Gold" } |
overridesoptional | Deep-merged into the template before variable substitution. Only the content key is accepted: { "content": { "fieldName": "value or {{placeholder}}" } } |
Idempotency-Keyheader | Optional request header. If a pass was already created with this key, the original response is returned. Returns 409 IDEMPOTENCY_KEY_CONFLICT if the key is reused with a different template or recipient. |
Example request
{
"template_id": "tmpl_a1b2c3d4",
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Smith",
"external_id": "usr_999",
"tags": ["vip", "london"],
"variables": {
"points": "1,250",
"tier": "Gold"
},
"overrides": {
"content": {
"headerField": "{{tier}} MEMBER",
"subtitle": "{{first_name}} — {{points}} pts"
}
}
}
Overrides are deep-merged into the template before {{placeholder}} substitution, so they can contain their own placeholders. In the example above, {{tier}} and {{first_name}} are resolved from data and recipient just like any other variable.
Response 201 — created
{
"pass_id": "pass_01j9...",
"external_id": "usr_999",
"status": "active",
"apple": {
"available": true,
"download_url": "https://api.walletpassapi.com/v1/passes/pass_01j9.../pkpass"
},
"google": {
"available": true,
"save_url": "https://pay.google.com/gp/v/save/eyJhbGc..."
},
"created_at": "2025-06-01T12:00:00.000Z"
}
Response 200 — updated existing pass
{
"pass_id": "pass_01j9...",
"external_id": "usr_999",
"status": "active",
"apple": {
"available": true,
"download_url": "https://api.walletpassapi.com/v1/passes/pass_01j9.../pkpass"
},
"google": {
"available": true,
"save_url": "https://pay.google.com/gp/v/save/eyJhbGc..."
},
"created_at": "2025-01-15T10:00:00.000Z"
}
/v1/passes
List all passes in the workspace, newest first. Each pass includes full identity and variable fields.
Query parameters
| Parameter | Description |
|---|---|
limitoptional | Results per page — default 50, max 100 |
offsetoptional | Pagination offset — default 0 |
template_idoptional | Filter by template ID |
tagoptional | Filter to passes containing this tag |
emailoptional | Filter by recipient email address |
external_idoptional | Filter by your external identifier |
statusoptional | Filter by pass status: active, voided, or expired |
Response 200
{
"data": [
{
"id": "pass_01j9...",
"template_id": "tmpl_a1b2c3d4",
"serial_number": "SN-XXXXXXXX",
"pass_type": "storeCard",
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Smith",
"sms_mobile": null,
"external_id": "usr_999",
"variables": { "points": "1,250", "tier": "Gold" },
"tags": ["vip", "london"],
"device_count": 1,
"pkpass_url": "https://api.walletpassapi.com/v1/passes/pass_01j9.../pkpass",
"created_at": "2025-06-01T12:00:00.000Z",
"updated_at": "2025-06-01T12:00:00.000Z"
}
],
"pagination": { "total": 847, "limit": 50, "offset": 0, "has_more": true }
}
/v1/passes/:id
Retrieve a pass with full identity, variable, device, and wallet URL fields.
Response 200
{
"pass_id": "pass_01j9...",
"template_id": "tmpl_a1b2c3d4",
"external_id": "usr_999",
"status": "active",
"recipient": {
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Smith",
"sms_number": null
},
"data": {
"points": "1,250",
"tier": "Gold"
},
"module": {
"mode": "loyalty_module",
"loyalty_member_id": "lm_01j9..."
},
"apple": { "download_url": "https://api.walletpassapi.com/v1/passes/pass_01j9.../pkpass" },
"google": { "save_url": "https://pay.google.com/gp/v/save/..." },
"tags": ["vip", "london"],
"created_at": "2025-06-01T12:00:00.000Z",
"updated_at": "2025-06-01T12:00:00.000Z"
}
/v1/passes/:id
/v1/passes/:id
spec-canonical alias
Update a pass and push the change silently to all installed Apple Wallet devices. Pass variables to re-substitute from the template. Pass identity fields to update the person record. Pass silent: true to skip the Apple push.
Request body
| Field | Description |
|---|---|
variablesoptional | Updated variable values — merged with existing, re-substituted into the template, then pushed to devices |
overridesoptional | Deep-merged into the template before variable substitution. Only the content key is accepted: { "content": { "fieldName": "value" } } |
emailoptional | Update recipient email |
first_nameoptional | Update first name |
last_nameoptional | Update last name |
sms_mobileoptional | Update mobile number (alias: sms_number) |
external_idoptional | Update your own ID for this person |
tagsoptional | Replace the pass's tag array |
silentoptional | Set true to update data without sending an Apple push notification |
Example — update points balance
{
"variables": { "points": "2,000" }
}
Response 200
{
"pass_id": "pass_01j9...",
"status": "active",
"updated": true,
"apple": { "updated": true },
"google": { "updated": true }
}
If any Apple push notifications failed, a push_errors array is also present.
/v1/passes/:id
Delete a pass and send a removal push to registered Apple Wallet devices.
Response 200
{ "deleted": true, "push_sent_to": 1 }
/v1/passes/:id/notify
Push a notification message to all devices holding this pass. The message appears on the lock screen and is written to a back field on the pass.
Request body
| Field | Description |
|---|---|
messagerequired | Notification message text |
labeloptional | Back-field label (defaults to Last Message) |
Response 200
{
"ok": true,
"push_sent_to": 1,
"pkpass_url": "https://.../pkpass",
"google_save_url": "https://pay.google.com/..."
}
/v1/passes/:id/pkpass
No auth required
Download the signed .pkpass bundle. This URL is intentionally public — link to it in email or a web page so recipients can tap to add the pass to Apple Wallet.
Response 200
Binary .pkpass file with Content-Type: application/vnd.apple.pkpass.
/v1/passes/:id/google-jwt
Generate a fresh Google Wallet JWT and save URL. Use google_save_url as the href for a Save to Google Wallet button.
Response 200
{
"google_jwt": "eyJhbGciOiJSUzI1NiJ9...",
"google_save_url": "https://pay.google.com/gp/v/save/eyJhbGc..."
}
/v1/passes/:id/events
Returns paginated event history for a pass — issuance, updates, device registrations, and more.
Query parameters
| Parameter | Description |
|---|---|
limitoptional | Results per page — default 50, max 100 |
offsetoptional | Pagination offset — default 0 |
Response 200
{
"data": [
{
"id": "evt_01j9...",
"pass_id": "pass_01j9...",
"event_type": "pass.issued",
"created_at": "2025-06-01T12:00:00.000Z"
}
],
"total": 12,
"limit": 50,
"offset": 0
}
Batch
Issue many passes in a single request. Each item in the passes array follows the same format as POST /v1/issue. Passes are processed asynchronously — poll the job endpoint or supply a webhook_url to receive a batch.completed event.
/v1/passes/batch
Queue a batch of up to 1,000 passes for async creation.
Request body
| Field | Description |
|---|---|
passesrequired | Array of issue objects — same shape as POST /v1/issue |
webhook_urloptional | URL to notify on completion — overrides the account-level default |
Example
{
"passes": [
{ "template_id": "tmpl_a1b2c3d4", "email": "alice@example.com", "variables": { "points": "500" } },
{ "template_id": "tmpl_a1b2c3d4", "email": "bob@example.com", "variables": { "points": "1200" } }
]
}
Response 202 Accepted
{
"job_id": "job_01j9...",
"status": "queued",
"pass_count": 2,
"created_at": "2025-06-01T12:00:00.000Z"
}
/v1/passes/batch/:job_id
Poll the status of a batch job. Status transitions: queued → processing → completed or failed.
Response 200
{
"job_id": "job_01j9...",
"status": "completed",
"pass_count": 2,
"completed_count": 2,
"failed_count": 0,
"results": [
{ "index": 0, "status": "ok", "id": "pass_01...", "error": null, "error_message": null },
{ "index": 1, "status": "ok", "id": "pass_02...", "error": null, "error_message": null }
],
"created_at": "2025-06-01T12:00:00.000Z",
"completed_at": "2025-06-01T12:00:05.000Z"
}
Void Pass
Permanently mark a pass as voided and push the state to installed devices.
/v1/passes/:id/void
Auth: wk_ key
Marks a pass as voided and pushes the voided state to all installed Apple Wallet devices. Fires a pass.voided webhook. This action cannot be undone. No request body is required.
Response 200
{
"pass_id": "pass_01j9...",
"status": "voided"
}
Response 409 Conflict
{
"error": {
"code": "PASS_ALREADY_VOIDED",
"message": "This pass has already been voided.",
"request_id": "req_01j9abc..."
}
}
API Keys
Create and revoke named API keys scoped to an account or a specific workspace.
/v1/api-keys
Auth: sk_ key
Returns all named API keys for the account. Raw key values are never returned after creation.
Response 200
[
{
"id": "key_01j9...",
"name": "CI/CD key",
"scope": "workspace",
"workspace_id": "ws_01j9...",
"last_used_at": "2025-06-14T10:22:00.000Z",
"revoked_at": null,
"created_at": "2025-05-01T09:00:00.000Z"
}
]
/v1/api-keys
Auth: sk_ key
Creates a new named API key. The raw key value is returned only once in this response — store it securely.
Request body
| Field | Description |
|---|---|
namerequired | Human-readable label for this key |
scoperequired | account or workspace |
workspace_idoptional | Required when scope is workspace |
Example
{
"name": "CI/CD key",
"scope": "workspace",
"workspace_id": "ws_01j9..."
}
Response 201 Created
{
"id": "key_01j9...",
"name": "CI/CD key",
"scope": "workspace",
"key": "wk_live_..."
}
/v1/api-keys/:id
Auth: sk_ key
Permanently revokes a named API key. Any requests using this key will immediately receive a 401 Unauthorized response.
Response 200
{
"revoked": true
}
Webhook Endpoints
Manage the registered HTTP endpoints that receive webhook event deliveries.
/v1/webhooks/endpoints
Auth: sk_ key
Returns all registered webhook endpoints for the account.
Response 200
[
{
"id": "whe_01j9...",
"name": "Production",
"url": "https://yourapp.com/webhooks/wallet",
"events": ["pass.created", "pass.updated"],
"workspace_id": null,
"is_active": true,
"created_at": "2025-05-01T09:00:00.000Z"
}
]
/v1/webhooks/endpoints
Auth: sk_ key
Registers a new webhook endpoint. The webhook_secret for signature verification is returned only once — store it securely.
Request body
| Field | Description |
|---|---|
urlrequired | HTTPS URL to receive event payloads |
nameoptional | Human-readable label |
eventsoptional | Array of event names to subscribe to. Pass an empty array to receive all events. |
workspace_idoptional | null for account-level; set to a workspace ID to scope events to that workspace only |
Example
{
"url": "https://yourapp.com/webhooks/wallet",
"name": "Production",
"events": ["pass.created", "pass.updated"],
"workspace_id": null
}
Response 201 Created
{
"id": "whe_01j9...",
"name": "Production",
"url": "https://yourapp.com/webhooks/wallet",
"events": ["pass.created", "pass.updated"],
"workspace_id": null,
"is_active": true,
"webhook_secret": "whsec_...",
"created_at": "2025-06-01T09:00:00.000Z"
}
/v1/webhooks/endpoints/:id
Auth: sk_ key
Updates a registered endpoint. All fields are optional — only provided fields are changed.
Request body
| Field | Description |
|---|---|
urloptional | New destination URL |
nameoptional | New label |
eventsoptional | Replace the subscribed event list |
is_activeoptional | Set to false to pause deliveries without deleting |
rotate_secretoptional | Set to true to generate a new signing secret — returned once in the response |
Response 200
{
"id": "whe_01j9...",
"name": "Production",
"url": "https://yourapp.com/webhooks/wallet",
"events": ["pass.created", "pass.updated", "pass.deleted"],
"is_active": true,
"webhook_secret": "whsec_..."
}
webhook_secret is only present in the response when rotate_secret: true was sent.
/v1/webhooks/endpoints/:id
Auth: sk_ key
Deletes a registered endpoint. All future deliveries to this endpoint are stopped immediately.
Response 200
{
"deleted": true
}
/v1/webhooks/endpoints/:id/deliveries
Auth: sk_ key
Returns the last 50 delivery attempts for this endpoint, newest first. Use this to debug failed deliveries.
Response 200
[
{
"id": "del_01j9...",
"event": "pass.created",
"status": 200,
"succeeded": true,
"attempt": 1,
"error": null,
"created_at": "2025-06-14T11:00:00.000Z"
},
{
"id": "del_01j8...",
"event": "pass.updated",
"status": 503,
"succeeded": false,
"attempt": 3,
"error": "upstream connect error or disconnect/reset before headers",
"created_at": "2025-06-14T10:55:00.000Z"
}
]
/v1/webhooks/endpoints/:id/test
Auth: sk_ key
Fires a test event to the endpoint URL and logs the delivery. Useful for verifying your endpoint is reachable and correctly validating signatures.
Response 200 — delivered
{
"delivered": true,
"status": 200
}
Response 200 — failed
{
"delivered": false,
"error": "Connection refused"
}
Loyalty
Loyalty
Manage loyalty schemes and member point balances. All loyalty endpoints require a wk_… workspace key.
/v1/loyalty-schemes
Auth: wk_ key
Create a new loyalty scheme.
Request body
| Field | Description |
|---|---|
namerequired | Display name for the scheme |
points_labeloptional | Label shown for the points currency — default Points |
template_idoptional | Pass template to associate with this scheme |
Response 201 Created
{
"id": "ls_01j9...",
"name": "Coffee Rewards",
"points_label": "Stars",
"workspace_id": "ws_01j9...",
"template_id": "tmpl_a1b2c3d4",
"created_at": "2025-06-01T12:00:00.000Z"
}
/v1/loyalty-schemes
Auth: wk_ key
List all loyalty schemes in the workspace.
Response 200
{
"data": [
{
"id": "ls_01j9...",
"name": "Coffee Rewards",
"points_label": "Stars",
"workspace_id": "ws_01j9...",
"template_id": "tmpl_a1b2c3d4",
"member_count": 142,
"created_at": "2025-06-01T12:00:00.000Z"
}
],
"total": 2
}
/v1/loyalty-schemes/:id
Auth: wk_ key
Retrieve a single loyalty scheme including member count.
Response 200
{
"id": "ls_01j9...",
"name": "Coffee Rewards",
"points_label": "Stars",
"workspace_id": "ws_01j9...",
"template_id": "tmpl_a1b2c3d4",
"member_count": 142,
"created_at": "2025-06-01T12:00:00.000Z"
}
/v1/loyalty-schemes/:id
Auth: wk_ key
Update an existing loyalty scheme. All fields are optional — only provided fields are changed.
Request body
| Field | Description |
|---|---|
nameoptional | New display name |
points_labeloptional | Label shown for the points currency |
template_idoptional | Pass template to associate with this scheme |
Response 200
{
"id": "ls_01j9...",
"name": "Coffee Rewards (updated)",
"points_label": "Stars",
"workspace_id": "ws_01j9...",
"template_id": "tmpl_a1b2c3d4",
"updated_at": "2026-06-17T09:00:00.000Z"
}
/v1/loyalty-schemes/:id
Auth: wk_ key
Delete a loyalty scheme.
Response 200
{
"deleted": true
}
/v1/loyalty-members
Auth: wk_ key
List all loyalty members in the workspace. Supports limit (default 50, max 100) and offset pagination.
Query parameters
| Parameter | Description |
|---|---|
limitoptional | Results per page — default 50, max 100 |
offsetoptional | Pagination offset — default 0 |
Response 200
{
"data": [
{
"id": "pass_01j9...",
"scheme_id": "ls_01j9...",
"points": 1250,
"email": "jane@example.com",
"external_id": "usr_999",
"created_at": "2025-06-01T12:00:00.000Z",
"updated_at": "2025-06-01T12:00:00.000Z"
}
],
"total": 142,
"limit": 50,
"offset": 0
}
/v1/loyalty-members/:id
Auth: wk_ key
Retrieve a loyalty member including current points balance and tier.
Response 200
{
"id": "lm_01j9...",
"scheme_id": "ls_01j9...",
"pass_id": "pass_01j9...",
"email": "jane@example.com",
"points_balance": 100,
"tier": "Gold",
"joined_at": "2025-06-01T12:00:00.000Z"
}
/v1/loyalty-members/:id/points
Auth: wk_ key
Adjust a member's points balance. Positive delta earns points, negative spends. Returns 422 insufficient_points if the resulting balance would be negative.
Two request formats are accepted:
Format A — delta (simple)
| Field | Description |
|---|---|
deltarequired | Points to add (positive) or deduct (negative) |
reasonoptional | Human-readable reason, e.g. purchase |
reference_idoptional | Your external reference, e.g. an order ID |
idempotency_keyoptional | Unique key to prevent duplicate transactions on retry |
Format B — spec-canonical (typed)
| Field | Description |
|---|---|
typerequired | earn_points | redeem_points | adjust |
pointsrequired | Absolute number of points (always positive) |
external_idoptional | Your external reference for this transaction |
Example (Format A)
{
"delta": 50,
"reason": "purchase",
"reference_id": "order_123",
"idempotency_key": "txn_abc"
}
Example (Format B)
{
"type": "earn_points",
"points": 50,
"external_id": "order_123"
}
Response 200
{
"member_id": "lm_01j9...",
"delta": 50,
"balance_after": 150,
"transaction_id": "lt_01j9..."
}
Response 422
{
"error": {
"code": "INSUFFICIENT_POINTS",
"message": "Insufficient points balance.",
"request_id": "req_01j9abc..."
}
}
/v1/loyalty-members/:id/transactions
Auth: wk_ key
List point transactions for a loyalty member. Supports limit (default 50, max 100) and cursor pagination.
Response 200
{
"data": [
{
"id": "lt_01j9...",
"delta": 50,
"balance_after": 150,
"reason": "purchase",
"reference_id": "order_123",
"created_at": "2025-06-01T12:00:00.000Z"
}
],
"total": 12,
"limit": 50,
"offset": 0
}
Coupons
Coupons
Manage coupon schemes and record redemptions. All coupon endpoints require a wk_… workspace key.
/v1/coupon-schemes
Auth: wk_ key
Create a new coupon scheme.
Request body
| Field | Description |
|---|---|
namerequired | Display name for the scheme |
coupon_typeoptional | single_use | multi_use | limited_use — default single_use |
offeroptional | Human-readable offer description, e.g. 20% off your next purchase |
valid_fromoptional | ISO 8601 datetime before which redemptions are refused with COUPON_NOT_YET_VALID |
redemption_limitoptional | Max redemptions per pass — null for unlimited |
expires_atoptional | ISO 8601 expiry datetime — passes cannot be redeemed after this time |
template_idoptional | Pass template to associate with this scheme |
Response 201 Created
{
"id": "cs_01j9...",
"name": "Summer Sale",
"coupon_type": "single_use",
"offer": "20% off your next purchase",
"valid_from": null,
"redemption_limit": 1,
"expires_at": "2026-08-31T23:59:59.000Z",
"workspace_id": "ws_01j9...",
"template_id": "tmpl_a1b2c3d4",
"created_at": "2025-06-01T12:00:00.000Z"
}
/v1/coupon-schemes
Auth: wk_ key
List all coupon schemes in the workspace.
Response 200
{
"data": [
{
"id": "cs_01j9...",
"name": "Summer Sale",
"redemption_limit": 1,
"expires_at": "2026-08-31T23:59:59.000Z",
"workspace_id": "ws_01j9...",
"template_id": "tmpl_a1b2c3d4",
"redemption_count": 38,
"created_at": "2025-06-01T12:00:00.000Z"
}
],
"total": 3
}
/v1/coupon-schemes/:id
Auth: wk_ key
Retrieve a single coupon scheme including redemption count.
Response 200
{
"id": "cs_01j9...",
"name": "Summer Sale",
"redemption_limit": 1,
"expires_at": "2026-08-31T23:59:59.000Z",
"workspace_id": "ws_01j9...",
"template_id": "tmpl_a1b2c3d4",
"redemption_count": 38,
"created_at": "2025-06-01T12:00:00.000Z"
}
/v1/coupon-schemes/:id
Auth: wk_ key
Update an existing coupon scheme. All fields are optional — only provided fields are changed.
Request body
| Field | Description |
|---|---|
nameoptional | New display name |
redemption_limitoptional | Max redemptions per pass — null for unlimited |
expires_atoptional | ISO 8601 expiry datetime — null to remove expiry |
template_idoptional | Pass template to associate with this scheme |
Response 200
{
"id": "cs_01j9...",
"name": "Summer Sale (updated)",
"redemption_limit": 2,
"expires_at": "2026-09-30T23:59:59.000Z",
"workspace_id": "ws_01j9...",
"template_id": "tmpl_a1b2c3d4",
"updated_at": "2026-06-17T09:00:00.000Z"
}
/v1/coupon-schemes/:id
Auth: wk_ key
Delete a coupon scheme.
Response 200
{
"deleted": true
}
/v1/coupons/:pass_id/redeem
Auth: wk_ key
Record a coupon redemption for a pass. Returns 422 if the scheme is expired or the pass has reached its redemption limit.
Request body
| Field | Description |
|---|---|
scheme_idrequired | ID of the coupon scheme to redeem against |
reference_idoptional | Your external reference, e.g. a POS transaction ID |
idempotency_keyoptional | Unique key to prevent duplicate redemptions on retry |
Example
{
"scheme_id": "cs_01j9...",
"reference_id": "pos_transaction_456",
"idempotency_key": "redeem_abc"
}
Response 201 Created
{
"redemption_id": "cr_01j9...",
"pass_id": "pass_01j9...",
"scheme_id": "cs_01j9...",
"redeemed_at": "2025-06-01T12:00:00.000Z"
}
Errors 422
{ "error": "scheme_expired" }
{ "error": "redemption_limit_reached" }
/v1/coupons/:pass_id/redemptions
Auth: wk_ key
List redemptions for a pass. Optionally filter by scheme_id query parameter.
Response 200
{
"data": [
{
"id": "cr_01j9...",
"scheme_id": "cs_01j9...",
"scheme_name": "Summer Sale",
"redeemed_at": "2025-06-01T12:00:00.000Z",
"reference_id": "pos_transaction_456"
}
]
}
Usage
Retrieve operation usage summaries. All usage endpoints require an account sk_… key.
/v1/usage
Auth: sk_ key
Returns an account-wide usage summary including total operations, allocation, and per-workspace breakdown.
Response 200
{
"plan": "starter",
"total_ops": 10000,
"ops_used": 1420,
"ops_remaining": 8580,
"billing_period_start": "2026-06-01T00:00:00.000Z",
"billing_period_end": "2026-07-01T00:00:00.000Z",
"workspaces": [
{ "id": "ws_01j9...", "name": "Production", "ops_used": 1200 },
{ "id": "ws_02j9...", "name": "Staging", "ops_used": 220 }
]
}
/v1/usage/workspaces/:workspace_id
Auth: sk_ key
Returns a usage summary for a specific workspace.
Response 200
{
"workspace_id": "ws_01j9...",
"workspace_name": "Production",
"ops_allocated": 5000,
"ops_used": 1200,
"ops_remaining": 3800,
"billing_period_start": "2026-06-01T00:00:00.000Z",
"billing_period_end": "2026-07-01T00:00:00.000Z"
}
Credentials
Manage workspace-level Apple and Google signing credentials. All credentials endpoints require a wk_… workspace key. Workspace credentials override account-level credentials for all passes in that workspace.
/v1/credentials
Auth: wk_ key
Returns credential metadata for the workspace — which providers are configured and key identifiers. Raw credential values are never returned.
Response 200
{
"apple": {
"configured": true,
"pass_type_identifier": "pass.com.example.mypass",
"team_id": "XXXXXXXXXX",
"apns_key_id": "YYYYYYYYYY"
},
"google": {
"configured": true,
"issuer_id": "3388000000012345678",
"service_account_email": "wallet@my-project.iam.gserviceaccount.com"
}
}
/v1/credentials/apple
Auth: wk_ key
Save Apple Wallet signing credentials for the workspace. All binary values must be base64-encoded. Replaces any existing Apple credentials.
Request body
| Field | Description |
|---|---|
apple_p12_certrequired | Pass signing certificate (.p12 file), base64-encoded |
apple_p12_passwordrequired | Password protecting the .p12 file |
apple_pass_type_identifierrequired | Pass Type ID, e.g. pass.com.example.mypass |
apple_team_idrequired | 10-character Apple Developer Team ID |
apple_apns_private_keyoptional | APNs auth key (.p8 contents), base64-encoded |
apple_apns_key_idoptional | 10-character APNs Key ID |
Response 200
{ "ok": true }
/v1/credentials/google
Auth: wk_ key
Save Google Wallet signing credentials for the workspace. All binary values must be base64-encoded. Replaces any existing Google credentials.
Request body
| Field | Description |
|---|---|
google_issuer_idrequired | Issuer ID from Google Pay & Wallet Console |
google_service_account_emailrequired | Service account email address |
google_service_account_keyrequired | Service account JSON key file, base64-encoded |
Response 200
{ "ok": true }
/v1/credentials/apple
Auth: wk_ key
Remove Apple signing credentials from the workspace. The workspace will fall back to account-level credentials if available.
Response 200
{ "deleted": true }
/v1/credentials/google
Auth: wk_ key
Remove Google signing credentials from the workspace. The workspace will fall back to account-level credentials if available.
Response 200
{ "deleted": true }
Webhooks
Real-time event notifications delivered to your endpoint via HTTP POST.
Overview
Set a webhook_url on your account or workspace and the API will POST a JSON payload whenever a notable event occurs.
Payload envelope
{
"id": "evt_01j9...",
"type": "pass.updated",
"account_id": "acc_01j9...",
"workspace_id": "ws_01j9...",
"created_at": "2025-06-01T12:00:00.000Z",
"data": { ... }
}
Note: the envelope field is type (not event).
Signature verification
Every delivery includes three headers:
WalletPassAPI-Signature: v1=<hex-digest>— HMAC-SHA256 oftimestamp.bodyWalletPassAPI-Timestamp: <unix-epoch>— seconds since epochWalletPassAPI-Event-Id: evt_<id>— unique event identifier
The signature is computed as: HMAC-SHA256(secret, timestamp + "." + rawBody)
import crypto from 'crypto';
function verifyWebhook(rawBody, headers, secret) {
const timestamp = headers['walletpassapi-timestamp'];
const signature = headers['walletpassapi-signature']; // "v1="
const expected = 'v1=' + crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
Your webhook_secret is returned when you create an account or rotate it via PUT /v1/accounts/me with rotate_webhook_secret: true.
Events
pass.created
Fired when a new pass is created via POST /v1/issue or the batch endpoint.
{
"id": "evt_01j9...",
"type": "pass.created",
"account_id": "acc_01j9...",
"workspace_id": "ws_01j9...",
"created_at": "2025-06-01T12:00:00.000Z",
"data": {
"pass_id": "pass_01j9...",
"pkpass_url": "https://.../pkpass",
"google_save_url": "https://pay.google.com/gp/v/save/..."
}
}
pass.updated
Fired when a pass is updated via PUT /v1/passes/:id or when POST /v1/issue updates an existing pass.
{
"id": "evt_01j9...",
"type": "pass.updated",
"account_id": "acc_01j9...",
"workspace_id": "ws_01j9...",
"created_at": "2025-06-01T12:05:00.000Z",
"data": {
"pass_id": "pass_01j9...",
"pkpass_url": "https://.../pkpass",
"google_save_url": "https://pay.google.com/gp/v/save/..."
}
}
pass.create_failed
Fired when a pass creation attempt fails (e.g. signing error, invalid variables).
{
"id": "evt_01j9...", "type": "pass.create_failed",
"account_id": "acc_01j9...", "workspace_id": "ws_01j9...",
"created_at": "2025-06-01T12:00:00.000Z",
"data": { "error_code": "SIGNING_FAILED", "template_id": "tmpl_..." }
}
pass.update_failed
Fired when a pass update attempt fails.
{
"id": "evt_01j9...", "type": "pass.update_failed",
"account_id": "acc_01j9...", "workspace_id": "ws_01j9...",
"created_at": "2025-06-01T12:00:00.000Z",
"data": { "pass_id": "pass_01j9...", "error_code": "SIGNING_FAILED" }
}
pass.deleted
Fired when a pass is deleted via DELETE /v1/passes/:id.
{
"id": "evt_01j9...", "type": "pass.deleted",
"account_id": "acc_01j9...", "workspace_id": "ws_01j9...",
"created_at": "2025-06-01T12:10:00.000Z",
"data": { "pass_id": "pass_01j9..." }
}
coupon.redemption_failed
Fired when a coupon redemption is rejected (expired scheme, limit reached, or not yet valid).
{
"id": "evt_01j9...", "type": "coupon.redemption_failed",
"account_id": "acc_01j9...", "workspace_id": "ws_01j9...",
"created_at": "2025-06-01T12:00:00.000Z",
"data": { "pass_id": "pass_01j9...", "scheme_id": "cs_01j9...", "error_code": "SCHEME_EXPIRED" }
}
loyalty.transaction.created
Fired when a loyalty point transaction is recorded.
{
"id": "evt_01j9...", "type": "loyalty.transaction.created",
"account_id": "acc_01j9...", "workspace_id": "ws_01j9...",
"created_at": "2025-06-01T12:00:00.000Z",
"data": { "member_id": "lm_01j9...", "transaction_id": "lt_01j9...", "delta": 50, "balance_after": 150 }
}
device.registered
Fired when a user adds a pass to Apple Wallet on a device.
{
"id": "evt_01j9...", "type": "device.registered",
"account_id": "acc_01j9...", "workspace_id": "ws_01j9...",
"created_at": "2025-06-01T12:00:00.000Z",
"data": { "pass_id": "pass_01j9...", "device_library_identifier": "abc123def456..." }
}
device.unregistered
Fired when a user removes a pass from Apple Wallet.
{
"id": "evt_01j9...", "type": "device.unregistered",
"account_id": "acc_01j9...", "workspace_id": "ws_01j9...",
"created_at": "2025-06-01T12:00:00.000Z",
"data": { "pass_id": "pass_01j9...", "device_library_identifier": "abc123def456..." }
}
batch.completed
Fired when a batch job finishes processing. Check failed_count to detect partial failures.
{
"id": "evt_01j9...", "type": "batch.completed",
"account_id": "acc_01j9...", "workspace_id": "ws_01j9...",
"created_at": "2025-06-01T12:00:05.000Z",
"data": {
"job_id": "job_01j9...", "pass_count": 2,
"completed_count": 2, "failed_count": 0,
"results": [
{ "index": 0, "status": "ok", "id": "pass_01...", "error": null, "error_message": null },
{ "index": 1, "status": "ok", "id": "pass_02...", "error": null, "error_message": null }
]
}
}