WalletPass API Wallet Pass API / API Reference

API Reference

REST API for creating and managing Apple Wallet and Google Wallet passes.

Base URL

https://api.walletpassapi.com

How it works

  1. Design your pass template in the portal — set layout, colours, images, and define {variable} placeholders.
  2. Copy the template ID from the portal (or use GET /v1/templates).
  3. Call POST /v1/issue with the template ID and variable values to issue a personalised pass to each recipient.
  4. 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
400INVALID_REQUESTMissing or malformed request
400INVALID_OVERRIDEOverride object contained a key other than content
400RESERVED_VARIABLE_NAMEA system variable name was used in data
400MODULE_OWNED_VARIABLEVariable is managed by the loyalty/coupon module and cannot be set directly
401UNAUTHORIZEDMissing or invalid API key
402OPERATION_LIMITWorkspace operation limit reached
404NOT_FOUNDResource does not exist
404PASS_NOT_FOUNDPass does not exist
404TEMPLATE_NOT_FOUNDTemplate does not exist
409IDEMPOTENCY_KEY_CONFLICTSame idempotency key used with different template or recipient
422MISSING_PLACEHOLDER_VALUETemplate has missing_placeholder_behavior: "error" and a placeholder had no value
422INVALID_ASSETAsset upload failed validation (wrong mime type or exceeds 500 KB)
422INSUFFICIENT_POINTSLoyalty point deduction would result in negative balance
422COUPON_NOT_YET_VALIDCoupon scheme valid_from is in the future
422SCHEME_EXPIREDCoupon scheme has passed its expiry date
422REDEMPTION_LIMIT_REACHEDPass has reached the scheme's per-pass redemption limit
429RATE_LIMITEDToo many requests — back off and retry
500INTERNAL_ERRORUnexpected 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.

POST /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

FieldDescription
namerequiredDisplay name for the account
emailrequiredContact email address (must be unique)
webhook_urloptionalHTTPS 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.

GET /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"
}
PUT /v1/accounts/me

Update account settings. Only provided fields are changed.

Request body

FieldDescription
nameoptionalNew display name
webhook_urloptionalNew webhook URL (pass null to remove)
rotate_webhook_secretoptionalPass 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.

DELETE /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
}
POST /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."
}
PUT /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

FieldDescription
apple_p12_certPass signing certificate (.p12 file), base64-encoded
apple_p12_passwordPassword protecting the .p12 file
apple_pass_type_identifierPass Type ID, e.g. pass.com.example.mypass
apple_team_id10-character Apple Developer Team ID
apple_apns_private_keyAPNs auth key (.p8 contents), base64-encoded
apple_apns_key_id10-character APNs Key ID

Google Wallet fields

FieldDescription
google_issuer_idIssuer ID from Google Pay & Wallet Console
google_service_account_emailService account email address
google_service_account_keyService 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.

GET /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
}
POST /v1/workspaces

Create a new workspace. Returns the workspace wk_ API key once — save it immediately.

Request body

FieldDescription
namerequiredWorkspace display name
operations_allocatedoptionalOperations 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"
}
GET /v1/workspaces/:id

Retrieve a single workspace.

Response 200

{ "workspace": { "id": "ws_01j9...", "name": "Production", ... } }
PUT /v1/workspaces/:id

Update workspace name or operation allocation. Only provided fields are changed.

Request body

FieldDescription
nameoptionalNew display name
operations_allocatedoptionalNew operation allocation (must not exceed unallocated pool)

Response 200

{ "workspace": { "id": "ws_01j9...", "name": "Production (updated)", ... } }
DELETE /v1/workspaces/:id

Delete a workspace and all its passes. The default workspace cannot be deleted.

Response 200

{ "ok": true, "deleted_passes": 15 }
POST /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.

GET /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
}
GET /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
}
GET /v1/templates/:id/passes

List all passes issued from a specific template, with full identity and variable fields. Supports pagination.

Query parameters

ParameterDescription
limitoptionalResults per page — default 50, max 100
offsetoptionalPagination offset — default 0

Response 200

{
  "data": [ { "id": "pass_01j9...", "email": "jane@example.com", "variables": { "points": "1,250" }, ... } ],
  "total": 847,
  "limit": 50,
  "offset": 0
}
POST /v1/templates

Auth: wk_ key

Create a new pass template. Returns the created template.

Request body

FieldDescription
namerequiredDisplay name for the template
pass_dataoptionalLegacy flat pass layout object. Either pass_data or the nested template key is required.
templateoptionalSpec-canonical nested body — accepts content, appearance, and assets sub-objects (see example below)
pass_typeoptionalUnified pass type: generic | loyalty | coupon | event — default generic
platformsoptionalWhich wallet platforms to generate — ["apple"], ["google"], or ["apple","google"] (default both)
missing_placeholder_behavioroptionalerror | empty | keep — what to do when a placeholder has no value at issue time (default empty)
defaultsoptionalObject of default variable values applied before caller-supplied values — alias for defaults_json
assetsoptionalMap 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_schemaoptionalArray of variable definitions, e.g. [{ "name": "points", "label": "Points Balance", "type": "string" }]
module_modeoptionalOne of template_only | loyalty_module | coupon_module — default template_only
loyalty_scheme_idoptionalLink to an existing loyalty scheme
coupon_scheme_idoptionalLink to an existing coupon scheme

missing_placeholder_behavior

ValueBehaviour at issue / update time
empty (default)Unresolved {{var}} placeholders are replaced with an empty string
errorReturns 422 MISSING_PLACEHOLDER_VALUE if any placeholder has no value
keepUnresolved 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"
}
PATCH /v1/templates/:id

Auth: wk_ key

Update an existing template. All fields are optional — only provided fields are updated.

Request body

FieldDescription
nameoptionalNew display name
pass_dataoptionalReplacement pass layout object (legacy flat shape)
templateoptionalNested shape — content, appearance, assets
pass_typeoptionalgeneric | loyalty | coupon | event
platformsoptional["apple"] | ["google"] | ["apple","google"]
missing_placeholder_behavioroptionalerror | empty | keep
defaultsoptionalDefault variable values
assetsoptionalMap of asset name → { source } — see Create for full details
variable_schemaoptionalReplacement variable schema array
module_modeoptionalOne 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"
}
DELETE /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.

POST /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

FieldDescription
template_idrequiredID of the template to issue from (find in the portal or via GET /v1/templates)
emailrequiredRecipient email address — used as the upsert key
first_nameoptionalRecipient first name — also available as the {first_name} variable
last_nameoptionalRecipient last name — also available as {last_name} and {full_name}
sms_mobileoptionalMobile number — also available as {sms_mobile}. Alias: sms_number
external_idoptionalYour own ID for this person — when provided, used as the upsert key instead of email
tagsoptionalArray of strings for segmentation, e.g. ["vip", "london"]
variablesoptionalObject of key/value pairs to substitute into template placeholders, e.g. { "points": "1,250", "tier": "Gold" }
overridesoptionalDeep-merged into the template before variable substitution. Only the content key is accepted: { "content": { "fieldName": "value or {{placeholder}}" } }
Idempotency-KeyheaderOptional 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"
}
GET /v1/passes

List all passes in the workspace, newest first. Each pass includes full identity and variable fields.

Query parameters

ParameterDescription
limitoptionalResults per page — default 50, max 100
offsetoptionalPagination offset — default 0
template_idoptionalFilter by template ID
tagoptionalFilter to passes containing this tag
emailoptionalFilter by recipient email address
external_idoptionalFilter by your external identifier
statusoptionalFilter 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 }
}
GET /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"
}
PUT /v1/passes/:id
PATCH /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

FieldDescription
variablesoptionalUpdated variable values — merged with existing, re-substituted into the template, then pushed to devices
overridesoptionalDeep-merged into the template before variable substitution. Only the content key is accepted: { "content": { "fieldName": "value" } }
emailoptionalUpdate recipient email
first_nameoptionalUpdate first name
last_nameoptionalUpdate last name
sms_mobileoptionalUpdate mobile number (alias: sms_number)
external_idoptionalUpdate your own ID for this person
tagsoptionalReplace the pass's tag array
silentoptionalSet 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.

DELETE /v1/passes/:id

Delete a pass and send a removal push to registered Apple Wallet devices.

Response 200

{ "deleted": true, "push_sent_to": 1 }
POST /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

FieldDescription
messagerequiredNotification message text
labeloptionalBack-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/..."
}
GET /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.

GET /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..."
}
GET /v1/passes/:id/events

Returns paginated event history for a pass — issuance, updates, device registrations, and more.

Query parameters

ParameterDescription
limitoptionalResults per page — default 50, max 100
offsetoptionalPagination 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.

POST /v1/passes/batch

Queue a batch of up to 1,000 passes for async creation.

Request body

FieldDescription
passesrequiredArray of issue objects — same shape as POST /v1/issue
webhook_urloptionalURL 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"
}
GET /v1/passes/batch/:job_id

Poll the status of a batch job. Status transitions: queuedprocessingcompleted 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.

POST /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.

GET /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"
  }
]
POST /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

FieldDescription
namerequiredHuman-readable label for this key
scoperequiredaccount or workspace
workspace_idoptionalRequired 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_..."
}
DELETE /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.

GET /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"
  }
]
POST /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

FieldDescription
urlrequiredHTTPS URL to receive event payloads
nameoptionalHuman-readable label
eventsoptionalArray of event names to subscribe to. Pass an empty array to receive all events.
workspace_idoptionalnull 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"
}
PUT /v1/webhooks/endpoints/:id

Auth: sk_ key

Updates a registered endpoint. All fields are optional — only provided fields are changed.

Request body

FieldDescription
urloptionalNew destination URL
nameoptionalNew label
eventsoptionalReplace the subscribed event list
is_activeoptionalSet to false to pause deliveries without deleting
rotate_secretoptionalSet 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.

DELETE /v1/webhooks/endpoints/:id

Auth: sk_ key

Deletes a registered endpoint. All future deliveries to this endpoint are stopped immediately.

Response 200

{
  "deleted": true
}
GET /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"
  }
]
POST /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.

POST /v1/loyalty-schemes

Auth: wk_ key

Create a new loyalty scheme.

Request body

FieldDescription
namerequiredDisplay name for the scheme
points_labeloptionalLabel shown for the points currency — default Points
template_idoptionalPass 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"
}
GET /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
}
GET /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"
}
PATCH /v1/loyalty-schemes/:id

Auth: wk_ key

Update an existing loyalty scheme. All fields are optional — only provided fields are changed.

Request body

FieldDescription
nameoptionalNew display name
points_labeloptionalLabel shown for the points currency
template_idoptionalPass 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"
}
DELETE /v1/loyalty-schemes/:id

Auth: wk_ key

Delete a loyalty scheme.

Response 200

{
  "deleted": true
}
GET /v1/loyalty-members

Auth: wk_ key

List all loyalty members in the workspace. Supports limit (default 50, max 100) and offset pagination.

Query parameters

ParameterDescription
limitoptionalResults per page — default 50, max 100
offsetoptionalPagination 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
}
GET /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"
}
POST /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)

FieldDescription
deltarequiredPoints to add (positive) or deduct (negative)
reasonoptionalHuman-readable reason, e.g. purchase
reference_idoptionalYour external reference, e.g. an order ID
idempotency_keyoptionalUnique key to prevent duplicate transactions on retry

Format B — spec-canonical (typed)

FieldDescription
typerequiredearn_points | redeem_points | adjust
pointsrequiredAbsolute number of points (always positive)
external_idoptionalYour 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..."
  }
}
GET /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.

POST /v1/coupon-schemes

Auth: wk_ key

Create a new coupon scheme.

Request body

FieldDescription
namerequiredDisplay name for the scheme
coupon_typeoptionalsingle_use | multi_use | limited_use — default single_use
offeroptionalHuman-readable offer description, e.g. 20% off your next purchase
valid_fromoptionalISO 8601 datetime before which redemptions are refused with COUPON_NOT_YET_VALID
redemption_limitoptionalMax redemptions per pass — null for unlimited
expires_atoptionalISO 8601 expiry datetime — passes cannot be redeemed after this time
template_idoptionalPass 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"
}
GET /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
}
GET /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"
}
PATCH /v1/coupon-schemes/:id

Auth: wk_ key

Update an existing coupon scheme. All fields are optional — only provided fields are changed.

Request body

FieldDescription
nameoptionalNew display name
redemption_limitoptionalMax redemptions per pass — null for unlimited
expires_atoptionalISO 8601 expiry datetime — null to remove expiry
template_idoptionalPass 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"
}
DELETE /v1/coupon-schemes/:id

Auth: wk_ key

Delete a coupon scheme.

Response 200

{
  "deleted": true
}
POST /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

FieldDescription
scheme_idrequiredID of the coupon scheme to redeem against
reference_idoptionalYour external reference, e.g. a POS transaction ID
idempotency_keyoptionalUnique 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" }
GET /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.

GET /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 }
  ]
}
GET /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.

GET /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"
  }
}
POST /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

FieldDescription
apple_p12_certrequiredPass signing certificate (.p12 file), base64-encoded
apple_p12_passwordrequiredPassword protecting the .p12 file
apple_pass_type_identifierrequiredPass Type ID, e.g. pass.com.example.mypass
apple_team_idrequired10-character Apple Developer Team ID
apple_apns_private_keyoptionalAPNs auth key (.p8 contents), base64-encoded
apple_apns_key_idoptional10-character APNs Key ID

Response 200

{ "ok": true }
POST /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

FieldDescription
google_issuer_idrequiredIssuer ID from Google Pay & Wallet Console
google_service_account_emailrequiredService account email address
google_service_account_keyrequiredService account JSON key file, base64-encoded

Response 200

{ "ok": true }
DELETE /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 }
DELETE /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 of timestamp.body
  • WalletPassAPI-Timestamp: <unix-epoch> — seconds since epoch
  • WalletPassAPI-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 }
    ]
  }
}