Passmint
TemplatesIntegrationsDocsPricing
Log inGet started
Passmint

Apple and Google Wallet passes from one API. Built for people who ship.

  • GitHub
  • X
Product
  • Pass Designer
  • Developers
  • Distribution
  • Analytics
  • Templates
  • Integrations
  • Pricing
Resources
  • Free tools
  • Wallet card generator
  • pkpass validator
  • Pass anatomy
  • Live demo
  • Blog
  • Glossary
  • Compare
Developers
  • Documentation
  • API reference
  • Node SDK
  • Webhooks
  • MCP server
Company
  • Changelog
  • About
  • Contact
  • Support
  • Security
  • Terms
  • Privacy

© 2026 Passmint.

Apple Wallet & Google Wallet, one API.

Getting started

  • Overview
  • Quickstart

API

  • REST API reference
    • Authentication
    • Passes
    • Templates
    • Events
    • Webhooks
    • Errors
  • Pass images
    • Image slots
    • How images resolve
    • Stamp card example
    • Member photo example
    • Google Wallet
    • Privacy
  • Webhooks
    • Event types
    • Creating a webhook
    • Payload format
    • Verifying signatures
    • Retries
  • MCP server
    • Install
    • The endpoint
    • Other clients
    • Read-only mode
    • Tools
    • Things worth knowing
    • Example prompts
    • What it doesn't do
    • Rate limits
  • Node.js SDK
    • Configuration
    • Passes
    • Templates
    • Webhook signatures
    • Errors
    • Idempotency

Open source

  • Passmint Package
REST API · v1

API reference.

Base URL: https://api.passmint.com

01

Authentication

Pass your API key as a Bearer token in the Authorization header. Test keys use the pmk_test_ prefix and live keys use pmk_live_. The mode (test or live) is determined by the key prefix. Test keys are available on every plan; live keys require a paid plan.

Authorization: Bearer pmk_test_xxx

The OpenAPI 3.1 spec, including the webhook payloads under webhooks, is served at https://api.passmint.com/openapi.json.

Scopes and expiry

By default a key has full access and never expires. When you create a key in the dashboard you can optionally restrict it to specific scopes and give it an expiry of 30, 90 or 365 days. Keys created before these options existed, and keys created with Full access, stay unrestricted.

ScopeAllows
templates:readList and retrieve templates.
templates:writeCreate, update and archive templates, and upload or remove template images.
passes:readList and retrieve passes, and read a pass's events and redemptions.
passes:writeIssue, update and void passes, create download links, and upload or remove pass images.
webhooks:readList and retrieve webhooks, and list their deliveries.
webhooks:writeCreate, update and delete webhooks, run backfills, and replay deliveries.
events:readRead GET /v1/events and /v1/metrics/funnel. Per-pass events need passes:read.

/v1/me needs no scope. Tool calls made through the MCP serverobey the key’s scopes too.

A request outside the key’s scopes returns 403 insufficient_scope, with param naming the scope it needed. A key past its expiry returns 401 api_key_expired.

{
  "error": {
    "type": "authorization_error",
    "code": "insufficient_scope",
    "message": "This API key doesn't have the passes:write scope.",
    "param": "passes:write"
  }
}
GET/v1/me

Retrieve your account

Returns the organization the key belongs to, its mode, your plan, live-mode usage for the current billing period, and your own signing credentials. Pass issuance stops at api_calls.quota; other live writes continue up to api_calls.grace_ceiling. A null quota means your plan sets none. Credentials are metadata only.

curl https://api.passmint.com/v1/me \
  -H "Authorization: Bearer pmk_test_xxx"
{
  "object": "account",
  "organization_id": "org_xxx",
  "organization_name": "Acme",
  "organization_slug": "acme",
  "mode": "test",
  "plan": { "id": "pro", "name": "Pro" },
  "usage": {
    "period_start": "2026-09-01T00:00:00Z",
    "api_calls": { "used": 8120, "quota": 100000, "grace_ceiling": 120000 },
    "active_passes": { "used": 1340, "included": 5000, "ceiling": 10000 }
  },
  "credentials": {
    "apple_certificates": [
      {
        "id": "certSet_xxx",
        "name": "Production",
        "pass_type_identifier": "pass.com.acme.events",
        "status": "active",
        "expires_at": "2027-03-01T00:00:00Z"
      }
    ],
    "google_issuers": [
      {
        "id": "googleIssuer_xxx",
        "name": "Acme",
        "issuer_id": "3388000000012345678",
        "status": "active"
      }
    ]
  }
}
02

Idempotency

Send an Idempotency-Key header on POST and PATCH requests to safely retry without duplicating side effects. Replays within 24 hours return the cached response. Reusing a key with a different request body returns 409.

Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7
03

Response format

Single objects include an object field identifying the type. List endpoints wrap results in a standard envelope.

// Single object
{
  "object": "pass",
  "id": "pass_xxx",
  ...
}

// List
{
  "object": "list",
  "data": [...],
  "has_more": false
}

Pagination

List endpoints return the newest items first. Use limit to set the page size and starting_after to fetch the next page: pass the id of the last item you received. has_more is true when there are more items after this page. A limit of 0 returns an empty page.

curl "https://api.passmint.com/v1/passes?limit=50&starting_after=pass_xxx" \
  -H "Authorization: Bearer pmk_test_xxx"

A negative or non-numeric limit returns 400 invalid_limit. A starting_afterthat doesn’t exist, or belongs to another account, returns 400 invalid_cursor; so does one whose item has since been deleted, in which case start again from the first page. A cursor must be an id from the same list you are paging. Pagination applies to templates, passes, per-pass events, webhooks and webhook deliveries. GET /v1/webhooks with no parameters still returns every endpoint, and only pages when you send limit or starting_after. Redemptions accept limit only. GET /v1/events and redemptions keep their earlier handling: /v1/events starts from the first page when starting_after is unknown (no invalid_cursor), and neither returns invalid_limit; a limit is coerced and capped at 200.

04

Errors

Errors return a consistent JSON envelope with a type, optional code, human-readable message, and the offending param when applicable.

{
  "error": {
    "type": "invalid_request_error",
    "code": null,
    "message": "template_id is required",
    "param": "template_id"
  }
}
TypeStatusDescription
authentication_error401Invalid or missing API key
invalid_request_error400Malformed or invalid request body
authorization_error403Your plan does not include this capability
not_found_error404Resource does not exist
billing_error402Plan limit or quota exceeded
rate_limit_error429Too many requests, retry shortly
idempotency_error409Idempotency key reused with different body
api_error500Internal server error
05

Passes

Passes are the core resource. Each represents a single wallet pass issued from a template.

POST/v1/passes

Issue a pass

Create a new pass from a template. Supports idempotency.

template_idstringRequired. The template to issue from.
holder_emailstringEmail address of the pass holder.
holder_namestringFull name of the pass holder.
field_valuesRecord<string, string>Key-value pairs for template fields.
metadataRecord<string, unknown>Arbitrary metadata to attach to the pass.
image_variantstringRender the pass with this template image variant (one of the template's image_variants). Omit for the template's own images. An unknown name returns 400 with code unknown_image_variant.
curl https://api.passmint.com/v1/passes \
  -H "Authorization: Bearer pmk_test_xxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "template_id": "tmpl_xxx",
    "holder_email": "alice@example.com",
    "holder_name": "Alice Johnson",
    "field_values": { "seatNumber": "A12" },
    "metadata": { "order_id": "ord_123" }
  }'
{
  "object": "pass",
  "id": "pass_xxx",
  "short_id": "abc123",
  "template_id": "tmpl_xxx",
  "serial_number": "...",
  "mode": "test",
  "holder_email": "alice@example.com",
  "holder_name": "Alice Johnson",
  "field_values": { "seatNumber": "A12" },
  "voided": false,
  "voided_at": null,
  "metadata": { "order_id": "ord_123" },
  "image_variant": null,
  "images": {
    "icon": { "source": "template" },
    "logo": { "source": "template" },
    "strip": { "source": "template" }
  },
  "created_via_api": true,
  "url": "https://passmint.com/p/abc123",
  "download_url": "https://passmint.com/p/abc123/download",
  "created_at": "2026-04-16T12:00:00Z"
}
GET/v1/passes

List passes

Returns a paginated list of passes.

template_idstringFilter by template.
holder_emailstringFilter by holder email.
limitintegerNumber of results (default 50, max 100).
starting_afterstringCursor: the id of the last item on the previous page. See Pagination.
curl "https://api.passmint.com/v1/passes?template_id=tmpl_xxx&limit=10" \
  -H "Authorization: Bearer pmk_test_xxx"
{
  "object": "list",
  "data": [{ "object": "pass", "id": "pass_xxx", ... }],
  "has_more": false
}
GET/v1/passes/:id

Retrieve a pass

Returns a single pass by ID.

curl https://api.passmint.com/v1/passes/pass_xxx \
  -H "Authorization: Bearer pmk_test_xxx"
PATCH/v1/passes/:id

Update a pass

Update the field values, metadata or image variant of a pass. At least one of field_values, metadata or image_variant is required. Cannot update voided passes (returns 400). Live updates are pushed to installed passes automatically. Supports idempotency.

field_valuesRecord<string, string>Updated field values.
metadataRecord<string, unknown>Updated metadata.
image_variantstring | nullSwitch the pass to this template image variant; null switches back to the template's own images; omit to leave it unchanged. Changing only this is a valid update, and is re-signed and pushed like any other. An unknown name returns 400 with code unknown_image_variant.
curl -X PATCH https://api.passmint.com/v1/passes/pass_xxx \
  -H "Authorization: Bearer pmk_test_xxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "field_values": { "seatNumber": "B7" } }'

The response is the updated pass plus delivery and warnings. delivery reports what the update reached on each wallet platform. A platform is null when the pass has nothing on it.

{
  "object": "pass",
  "id": "pass_xxx",
  ...
  "delivery": {
    "apple": { "devices": 2, "pushed": 2, "failed": 0, "unregistered": 0 },
    "google": { "status": "updated" }
  },
  "warnings": []
}
delivery.appleobject | nulldevices: installed devices. pushed: devices Apple accepted the push for. failed: pushes Apple refused for a reason other than a dead token. unregistered: devices whose pass was deleted by the holder and are no longer tracked. Null when the pass has no Apple Wallet side.
delivery.google.statusstringupdated: Google Wallet accepted the change. not_saved: the holder has not saved the pass to Google Wallet, so there is nothing to update. failed: Google returned an error (see error). skipped: Google Wallet is not set up for this account.
delivery.google.errorstring?Present when status is failed.
delivery.apple.retrying / delivery.google.retryingtrue?Present when the delivery failed for a reason that may clear (Apple or Google busy, a network error) and Passmint queued an automatic retry: after 30 seconds, then 5 and 30 minutes, 2, 12 and 24 hours. Absent otherwise.
warningsstring[]Plain-language notes about delivery that did not go through. Empty when everything did. When a platform is being retried, its note ends with "Passmint will retry automatically." Match warnings by prefix or substring, not exact text.

A failed delivery does not fail the request. The change is saved, and the device picks it up the next time it refreshes the pass. When the failure may clear by itself, Passmint retries automatically and delivery says retrying: true. A pass.update_failedwebhook is sent when a failure will not clear by itself, or when the retries run out (7 attempts over about 38 hours); its payload is unchanged. If some of a pass's Apple devices fail for good and others only temporarily, the webhook is sent straight away, and again if the queued retry later runs out. Updates from the pass image routes retry Google Wallet the same way.

DELETE/v1/passes/:id

Void a pass

Voids a pass. The pass is not deleted. It is returned with voided: true and a voided_at timestamp.

Voiding re-signs the pass so Apple Wallet shows it as voided, and pushes it to installed devices. Devices show it as voided after they refresh, and the pass is still served to them. On Google Wallet the object is set to inactive; restoring a pass (staff only) sets it active again. Voiding never fails because a wallet platform did: warnings says when a device could not be told, and a pass.update_failed webhook fires with context void. When the failure may clear by itself, Passmint retries automatically and the webhook fires only if the retries run out. Calling DELETE again on a voided pass re-sends the voided version to Apple only if it never went out.

curl -X DELETE https://api.passmint.com/v1/passes/pass_xxx \
  -H "Authorization: Bearer pmk_test_xxx"
{
  "object": "pass",
  "id": "pass_xxx",
  "voided": true,
  "voided_at": "2026-04-16T14:30:00Z",
  "warnings": [],
  ...
}
05a

Pass images

A pass can have its own image of a slot, such as a member's photo, over its image variant's and its template's. Upload it after the pass is issued; images can't be sent when creating or updating a pass. See Pass images for how the layers resolve and how each maps to Google Wallet.

PUT/v1/passes/:id/images/:slot

Upload a pass image

Sets this pass's own image of strip, thumbnail or background, limited to the slots its template's style has (storeCard and coupon: strip; generic: thumbnail; eventTicket: all three; boardingPass: none). Any other slot returns 400 with code: "image_slot_not_allowed" and param: "slot". The pass is re-signed and pushed to installed devices, like any update, and the updated pass is returned with a warnings array (as from POST /v1/passes): if the Google Wallet object could not be updated, the upload still succeeds and warnings says why. A voided pass returns 409 (pass_voided). A request body over the 8 MB limit returns 413 (image_too_large). Two changes to the same pass at once: one wins, the other returns 409 (pass_images_changed) and can be retried. Uploading again replaces the image.

On Google Wallet, a pass stripis the pass's hero image. A pass thumbnail is the round logo on a generic pass, and an image module in the pass details on every other type. A background is Apple Wallet only.

datastringRequired. The image, base64-encoded PNG, JPEG or WebP, at any resolution. 8 MB limit.
curl -X PUT https://api.passmint.com/v1/passes/pass_xxx/images/thumbnail \
  -H "Authorization: Bearer pmk_test_xxx" \
  -H "Content-Type: application/json" \
  -d "{ \"data\": \"$(base64 -i photo.jpg)\" }"
{
  "object": "pass",
  "id": "pass_xxx",
  "images": {
    "icon": { "source": "template" },
    "logo": { "source": "template" },
    "thumbnail": { "source": "pass" }
  },
  ...,
  "warnings": []
}
DELETE/v1/passes/:id/images/:slot

Delete a pass image

Removes this pass's own image of a slot, so the slot falls back to its image variant or template. The pass is re-signed and pushed, and the updated pass is returned with warnings. Deleting a slot the pass has no image of changes nothing (it re-publishes the pass only if an earlier image change never fully reached it).

This is also how to erase a pass image: the stored image and its public copy are deleted, and the address Google Wallet loaded it from stops working, even if Google Wallet could not be updated. It works on a voided pass too: the image is erased, but the voided pass is not re-signed. Pass image addresses use a random token that changes on every upload or delete; the API never returns it.

curl -X DELETE https://api.passmint.com/v1/passes/pass_xxx/images/thumbnail \
  -H "Authorization: Bearer pmk_test_xxx"
POST/v1/passes/:id/download-links

Create an expiring download link

Mints a time-limited link to the pass page, the expiring alternative to the permanent url on the pass object. The link works exactly like the permanent one (Apple Wallet download, Google Wallet save, QR handoff) until expires_at, after which it stops resolving and the holder is told to request a fresh one. Mint a new link per delivery; links are independent and minting one never invalidates another.

Be clear-eyed about what expiry protects. The link itself, in an email, a support thread, a log line, a forwarded message. It does not time-limit the pass. A .pkpassdownloaded before expiry is a signed file on the holder's device, barcode included, and no link expiry can reach it. To control redemption, use redemption policies and the scanner, not download links.

To make expiring links the only way in, disabling the permanent URL entirely, set require_download_link on the template.

expires_inintegerLifetime in seconds. Default 3600 (one hour), minimum 60, maximum 604800 (7 days).
curl https://api.passmint.com/v1/passes/pass_xxx/download-links \
  -H "Authorization: Bearer pmk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "expires_in": 3600 }'
{
  "object": "pass.download_link",
  "pass_id": "pass_xxx",
  "url": "https://passmint.com/p/dl/pdl_mF4jK...",
  "expires_in": 3600,
  "expires_at": "2026-04-16T13:00:00Z",
  "created_at": "2026-04-16T12:00:00Z"
}

Download link object

objectstringAlways "pass.download_link".
pass_idstringThe pass this link resolves to.
urlstringThe link to hand to your customer. Contains the one-time-visible token. Passmint stores only a hash, so this is the only place the URL ever appears.
expires_inintegerLifetime in seconds, as requested.
expires_atstringISO 8601 expiry timestamp.
created_atstringISO 8601 creation timestamp.
GET/v1/passes/:id/redemptions

List a pass's scans

Every scan of the pass by a Passmint scanner, most recent first. A rejected scan carries its reason and did not change the pass.

limitintegerNumber of results (default 100, max 200).
curl "https://api.passmint.com/v1/passes/pass_xxx/redemptions?limit=20" \
  -H "Authorization: Bearer pmk_test_xxx"
{
  "object": "list",
  "data": [
    {
      "object": "redemption",
      "id": "rdm_xxx",
      "decision": "rejected",
      "reason": "already_redeemed",
      "scanner_name": "Front door",
      "scanned_at": "2026-04-16T19:02:00Z"
    }
  ],
  "has_more": false
}

Redemption object

idstringUnique identifier (rdm_xxx).
objectstringAlways "redemption".
decisionstring"accepted" or "rejected".
reasonstring | nullWhy a scan was rejected: already_redeemed, count_exhausted, voided, out_of_scope, wrong_mode, or ambiguous (a code that matches more than one pass). override on an accepted scan that an operator forced past the redemption policy. Null on a normal accept.
scanner_namestring | nullThe scanner that made the scan.
scanned_atstringISO 8601 timestamp.

Pass object

idstringUnique identifier (pass_xxx).
objectstringAlways "pass".
short_idstringShort URL slug.
template_idstringParent template (tmpl_xxx).
serial_numberstringUnique serial number for the pass.
modestring"test" or "live".
holder_emailstring | nullEmail of the pass holder.
holder_namestring | nullName of the pass holder.
field_valuesobjectKey-value pairs for template fields.
voidedbooleanWhether the pass has been voided.
voided_atstring | nullISO 8601 timestamp of when the pass was voided.
redeemed_countintegerHow many times the pass has been accepted by a scanner.
first_redeemed_atstring | nullISO 8601 timestamp of the first accepted scan.
metadataobjectArbitrary metadata attached to the pass.
image_variantstring | nullThe template image variant the pass renders with, or null for the template's own images.
imagesobjectEach image slot the pass renders, keyed by Apple slot name, with the layer it comes from: { "thumbnail": { "source": "pass" } }. source is "pass" (an image uploaded for this pass), "variant" (its image_variant) or "template". No URLs.
created_via_apibooleanWhether the pass was created via the API.
urlstringPublic URL for the pass landing page.
download_urlstringDirect download URL for the .pkpass file.
created_atstringISO 8601 creation timestamp.
06

Templates

Templates define the design and structure of your passes. Every pass is issued from a template.

POST/v1/templates

Create a template

Create a new pass template. Supports idempotency.

namestringRequired. Display name for the template.
typestringRequired. One of: event, membership, coupon, loyalty, generic.
apple_stylestringRequired. One of: eventTicket, generic, storeCard, coupon, boardingPass.
designobjectRequired. Design configuration for the template. See the design fields below and the TemplateDesign schema in the OpenAPI document.
design.issuerNamestringOptional, up to 64 characters. The issuer shown on the pass and in its notifications — Apple's organizationName, and Google's issuer name unless logoText is set. Defaults to your organization's name.
design.locationsobject[]Optional, up to 10 of { latitude, longitude, altitude?, relevantText? }. Apple Wallet suggests the pass on the lock screen near these places.
design.relevantDatestringOptional ISO 8601 timestamp with a timezone offset. Apple Wallet surfaces the pass around this time. Apple only.
design.google.loyaltyPointsobjectOptional, storeCard templates only. { fieldKey, label }: shows that field's value (or the field's default) as the Google Wallet loyalty balance on new passes, and on existing passes the next time they are updated, e.g. { "fieldKey": "count", "label": "Stamps" }. fieldKey must be a field declared in this design; label is up to 64 characters. The balance follows the field when a pass updates. Apple Wallet is unaffected. A mapping that names an undeclared field, or sits on a non-storeCard template, is accepted here with a warning (google_points_field, google_style) and the dashboard rejects it.
design.google.secondaryLoyaltyPointsobjectOptional. Same shape, shown in addition to the main balance (for example the next reward).
design.google.accountNameFieldKeystringOptional. A field whose value Google Wallet shows as the card holder's name. Leave it out to keep showing the description there, as Google cards always have. A pass with no value for the field shows no name.
redemption_policyobjectOptional. How scanners treat its passes: { "mode": "single_use" } (default), { "mode": "reusable" }, or { "mode": "count", "max_uses": n }.
require_download_linkbooleanDefault false. When true, the permanent pass URLs 404 and passes on this template are only reachable through expiring download links.
curl https://api.passmint.com/v1/templates \
  -H "Authorization: Bearer pmk_test_xxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "name": "VIP Concert Pass",
    "type": "event",
    "apple_style": "eventTicket",
    "design": {
      "description": "VIP concert pass",
      "issuerName": "Brixton Academy",
      "foregroundColor": "#ffffff",
      "backgroundColor": "#101010",
      "labelColor": "#cccccc",
      "primaryFields": [
        { "key": "event", "label": "Event", "defaultValue": null,
          "textAlignment": "left", "required": true }
      ],
      "secondaryFields": [],
      "auxiliaryFields": [],
      "backFields": [],
      "barcodeFormat": "PKBarcodeFormatQR",
      "barcodeMessageTemplate": "{{serialNumber}}",
      "locations": [
        { "latitude": 51.4651, "longitude": -0.1151,
          "relevantText": "Your ticket for tonight" }
      ]
    }
  }'

A design Wallet can't render, or one that breaks Apple's layout limits, is rejected with a 400 whose param names the field. That covers an unknown colour format, more fields than a slot shows, a field key with characters other than letters, numbers and underscores, a changeMessage without %@, an image the pass style can't display, or a malformed location:

{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_design",
    "param": "design.backgroundColor",
    "message": "design.backgroundColor must be an rgb(r, g, b), #rgb or #rrggbb colour"
  }
}

Two problems are saved with a warningsarray instead, because they don't stop the pass from rendering: a field key used twice, and a barcode placeholder other than {{serialNumber}} or {{fieldValues.<key>}}. Any other placeholder is encoded into the barcode as literal text.

{
  "object": "template",
  "id": "tmpl_xxx",
  "name": "VIP Concert Pass",
  "type": "event",
  "apple_style": "eventTicket",
  "design": { ... },
  "archived": false,
  "created_at": "2026-04-16T12:00:00Z",
  "updated_at": "2026-04-16T12:00:00Z"
}
GET/v1/templates

List templates

Returns a list of templates. Archived templates are left out unless you ask for them.

limitintegerNumber of results (default 50, max 100).
starting_afterstringCursor: the id of the last item on the previous page. See Pagination.
include_archivedbooleanInclude archived templates (default false).
curl "https://api.passmint.com/v1/templates?limit=10" \
  -H "Authorization: Bearer pmk_test_xxx"
GET/v1/templates/:id

Retrieve a template

Returns a single template by ID, including archived templates.

curl https://api.passmint.com/v1/templates/tmpl_xxx \
  -H "Authorization: Bearer pmk_test_xxx"
PATCH/v1/templates/:id

Update a template

Update a template's name, design, or archived status. All fields are optional.

namestringUpdated display name.
designobjectReplacement design configuration. Omit it (or send null) to keep the current design.
design.issuerNamestringOptional, up to 64 characters. The issuer shown on the pass and in its notifications — Apple's organizationName, and Google's issuer name unless logoText is set. Defaults to your organization's name.
design.locationsobject[]Optional, up to 10 of { latitude, longitude, altitude?, relevantText? }. Apple Wallet suggests the pass on the lock screen near these places.
design.relevantDatestringOptional ISO 8601 timestamp with a timezone offset. Apple Wallet surfaces the pass around this time. Apple only.
design.google.loyaltyPointsobjectOptional, storeCard templates only. { fieldKey, label }: shows that field's value (or the field's default) as the Google Wallet loyalty balance on new passes, and on existing passes the next time they are updated, e.g. { "fieldKey": "count", "label": "Stamps" }. fieldKey must be a field declared in this design; label is up to 64 characters. The balance follows the field when a pass updates. Apple Wallet is unaffected. A mapping that names an undeclared field, or sits on a non-storeCard template, is accepted here with a warning (google_points_field, google_style) and the dashboard rejects it.
design.google.secondaryLoyaltyPointsobjectOptional. Same shape, shown in addition to the main balance (for example the next reward).
design.google.accountNameFieldKeystringOptional. A field whose value Google Wallet shows as the card holder's name. Leave it out to keep showing the description there, as Google cards always have. A pass with no value for the field shows no name.
redemption_policyobjectOptional. How scanners treat its passes: { "mode": "single_use" } (default), { "mode": "reusable" }, or { "mode": "count", "max_uses": n }.
archivedbooleanSet to true to archive the template.
require_download_linkbooleanRestrict distribution to expiring download links. Takes effect immediately for every existing pass on the template, and permanent URLs already in the wild 404 while it is on.
curl -X PATCH https://api.passmint.com/v1/templates/tmpl_xxx \
  -H "Authorization: Bearer pmk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Updated Template Name" }'
DELETE/v1/templates/:id

Archive a template

Archives a template. Existing passes remain valid but no new passes can be issued from an archived template.

curl -X DELETE https://api.passmint.com/v1/templates/tmpl_xxx \
  -H "Authorization: Bearer pmk_test_xxx"
{
  "id": "tmpl_xxx",
  "deleted": true
}
POST/v1/templates/:id/republish

Push template changes to issued passes

Saving a template changes passes you issue from then on. To update passes already issued from it, call republish once your edits are done. It runs in the background, in the mode of your key. Google Wallet gets one class update that carries the issuer name, logo and hero to every holder, plus a per-pass update when fields, colours or the barcode changed (generic-style passes always get the per-pass update, because their card carries the title). Apple Wallet passes with an installed device are re-signed and pushed. Archived templates can be republished, since their passes are still in wallets. Supports idempotency; keys are kept per path.

Returns 202 with the republish. If one is already running for this template, it is returned with 200, and another runs when it finishes so the latest design reaches every pass. At most three republishes run per account and mode: a fourth gets 429 with code republish_in_progress, so wait for one to finish rather than retrying straight away. If republishing is briefly unavailable you get 503 with code republish_unavailable; try again in a few minutes.

A test key on a template that uses your own Google issuer does not rewrite the Google class that live passes share. A completed republish whose Google updates failed for good will not repeat those per-pass updates on the next republish of the same design. Change the design and republish again, or contact support.

curl -X POST https://api.passmint.com/v1/templates/tmpl_xxx/republish \
  -H "Authorization: Bearer pmk_test_xxx"
{
  "object": "template_republish",
  "id": "tmplRepub_xxx",
  "template_id": "tmpl_xxx",
  "livemode": false,
  "status": "queued",
  "google": { "status": "pending", "error": null, "objects": { "total": 2000, "done": 0, "failed": 0 } },
  "apple": { "total": 3000, "done": 0, "failed": 0 },
  "created_at": "2026-10-08T12:00:00.000Z",
  "completed_at": null
}

In the dashboard, the designer shows Push changes to N existing passes for the same thing.

GET/v1/templates/:id/republishes/:rid

Retrieve a template republish

The republish with live counts. status moves from queued to running to completed, and then a template.republished webhook is sent with the final counts.

PUT/v1/templates/:id/images/:slot

Upload a template image

Upload one image slot of the template: icon, logo, strip, thumbnail, background or footer. Send any resolution; it is resized to the slot's 1x and @2x sizes. The logo and strip are also used for Google Wallet (as its logo and hero image). Passes pick the change up the next time they are issued or updated.

The slot must be one the template's pass style has (a storeCard has icon, logo and strip, for example); any other slot returns 400 with code: "image_slot_not_allowed" and param: "slot". See Pass imagesfor every style's slots and how a pass's images resolve.

With variant, the image is stored as part of a named image variant instead: an alternate set of strip, thumbnail or background images that a pass selects with image_variant. A variant only needs the slots that differ; every other slot comes from the template. A variant's stripbecomes the pass's Google Wallet hero image.

datastringRequired. The image, base64-encoded PNG or JPEG. 8 MB limit.
variantstringWrite this image variant's copy of the slot. 1-32 characters of lowercase letters, digits, - and _, starting with a letter or digit. Only the style's strip, thumbnail and background can have variants (a boardingPass has none).
curl -X PUT https://api.passmint.com/v1/templates/tmpl_xxx/images/strip \
  -H "Authorization: Bearer pmk_test_xxx" \
  -H "Content-Type: application/json" \
  -d "{ \"variant\": \"3\", \"data\": \"$(base64 -i strip-3.png)\" }"
{
  "object": "template_image",
  "template_id": "tmpl_xxx",
  "slot": "strip",
  "variant": "3"
}
DELETE/v1/templates/:id/images/:slot

Delete a template image

Remove one image slot of the template, or with ?variant= of one of its image variants. A variant with no images left drops out of the template's image_variants. Any slot can be removed, including one the template's style doesn't use.

curl -X DELETE "https://api.passmint.com/v1/templates/tmpl_xxx/images/strip?variant=3" \
  -H "Authorization: Bearer pmk_test_xxx"
{
  "object": "template_image",
  "template_id": "tmpl_xxx",
  "slot": "strip",
  "variant": "3",
  "deleted": true
}
06a

Example: a stamp card

Image variants let one template show different artwork per pass. A ten-stamp loyalty card can carry a pre-rendered strip for every stamp count, and move each pass along as stamps are earned. Upload a strip per count once, as variants 0 to 9 and reward:

for v in 0 1 2 3 4 5 6 7 8 9 reward; do
  curl -X PUT https://api.passmint.com/v1/templates/tmpl_xxx/images/strip \
    -H "Authorization: Bearer pmk_test_xxx" \
    -H "Content-Type: application/json" \
    -d "{ \"variant\": \"$v\", \"data\": \"$(base64 -i strip-$v.png)\" }"
done

Issue each card on 0, then set the next variant with every stamp. The pass is re-signed and pushed to the holder's wallet, strip and all:

curl https://api.passmint.com/v1/passes \
  -H "Authorization: Bearer pmk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "template_id": "tmpl_xxx", "image_variant": "0" }'

curl -X PATCH https://api.passmint.com/v1/passes/pass_xxx \
  -H "Authorization: Bearer pmk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "image_variant": "4", "field_values": { "stamps": "4" } }'

Naming a variant the template does not have returns 400 with code: "unknown_image_variant" and param: "image_variant". Send "image_variant": nullto return a pass to the template's own images.

Template object

idstringUnique identifier (tmpl_xxx).
objectstringAlways "template".
namestringDisplay name.
typestringOne of: event, membership, coupon, loyalty, generic.
apple_stylestringOne of: eventTicket, generic, storeCard, coupon, boardingPass.
designobjectDesign configuration.
warningsstring[]Only on create and update responses, and only when the saved design has a duplicate field key or an unknown barcode placeholder.
archivedbooleanWhether the template is archived.
require_download_linkbooleanWhether passes on this template are reachable only through expiring download links.
redemption_policyobjectHow scanners treat its passes: { "mode": "single_use" }, { "mode": "reusable" }, or { "mode": "count", "maxUses": n }.
image_variantsstring[]Names of the template's image variants that hold at least one image, sorted.
created_atstringISO 8601 creation timestamp.
updated_atstringISO 8601 last update timestamp.
07

Events

Events track the lifecycle of passes, from creation through installation and removal.

GET/v1/events

List recent events

Returns recent events across all passes. An unknown starting_after returns the first page rather than an error, and limit is not validated with invalid_limit.

limitintegerNumber of results (default 100, max 200).
starting_afterstringCursor: the id of the last item on the previous page. See Pagination.
curl "https://api.passmint.com/v1/events?limit=50" \
  -H "Authorization: Bearer pmk_test_xxx"
GET/v1/passes/:id/events

List events for a pass

Returns events for a specific pass.

limitintegerNumber of results (default 100, max 200).
starting_afterstringCursor: the id of the last item on the previous page. See Pagination.
curl "https://api.passmint.com/v1/passes/pass_xxx/events?limit=20" \
  -H "Authorization: Bearer pmk_test_xxx"
{
  "object": "list",
  "data": [
    {
      "id": "psEvnt_xxx",
      "object": "event",
      "type": "pass.update_pushed",
      "api_version": "2026-06-26",
      "created_at": "2026-04-16T13:00:00.000Z",
      "idempotency_key": "psEvnt_xxx",
      "livemode": true,
      "data": {
        "object": {
          "pass": {
            "id": "pass_xxx",
            "short_id": "abc123",
            "template_id": "tmpl_xxx",
            "organization_id": "org_xxx",
            "serial_number": "SN-abc123",
            "holder": { "email_hash": "5d41...", "name_present": true },
            "voided": false,
            "image_variant": "3",
            "url": "https://passmint.com/p/abc123",
            "download_url": "https://passmint.com/p/abc123/download",
            "google_wallet_url": null
          }
        }
      },
      "source": { "platform": "apple", "unit": "device", "confidence": "exact" },
      "previous_attributes": null
    }
  ],
  "has_more": false
}

Event object

idstringUnique identifier (psEvnt_xxx).
objectstringAlways "event".
typestringEvent type (see below).
api_versionstringThe dated payload version. New fields are added without a new version.
created_atstringISO 8601 timestamp.
idempotency_keystringStable per occurrence. Use it to de-duplicate deliveries.
livemodebooleanWhether the pass is a live-mode pass.
data.object.passobjectSnapshot of the pass when the event was serialized (see below).
sourceobjectplatform (apple, google or null), unit (device or object) and confidence.
previous_attributesnullAlways null for now.

Event pass snapshot

idstringThe pass id.
short_idstringShort URL slug.
template_idstringThe template the pass was issued from.
organization_idstringThe owning organization.
serial_numberstringThe pass's serial number.
holderobjectemail_hash (SHA-256 of the lowercased email, or null) and name_present. Raw holder details are only returned by GET /v1/passes/:id.
voidedbooleanWhether the pass has been voided.
image_variantstring | nullThe template image variant the pass renders with, or null for the template's own images.
urlstringThe pass's landing page.
download_urlstring | nullApple Wallet download link, once the pass has been delivered to Apple Wallet.
google_wallet_urlstring | nullSave to Google Wallet link, once the pass has been delivered to Google Wallet.

Event types

TypeDescription
pass.issuedPass was created.
pass.add_intentSomeone downloaded the pass file or tapped Add to Google Wallet.
pass.added_to_walletPass was added to a wallet.
pass.update_pushedPass fields, metadata or image variant were updated, and at least one installed wallet was notified.
pass.update_deliveredA device fetched the updated pass (Apple-confirmed delivery).
pass.update_failedAn update or void was sent and Apple or Google Wallet refused it. Not sent when there was nobody to tell. One event per failing platform. See data.object.delivery_failure.
pass.removedPass was removed from a wallet.
pass.voidedPass was voided.
pass.redeemedPass was scanned and accepted.
template.republishedA template republish finished. Subscribe by name: "*" does not include it. The payload has data.object.template_republish and no pass.
08

Webhooks

Webhooks deliver real-time event notifications to your server. Passmint signs every payload so you can verify authenticity.

POST/v1/webhooks

Create a webhook

Register a new webhook endpoint. The response includes the secret field. This is the only time the secret is returned. Store it securely. Supports idempotency.

urlstringRequired. The URL to deliver events to.
eventsstring[]Required. Non-empty array of event types to subscribe to.
descriptionstringOptional description for the webhook.
enabledbooleanWhether the webhook is active (default true).

Valid event types: pass.issued, pass.add_intent, pass.added_to_wallet, pass.update_pushed, pass.update_delivered, pass.update_failed, pass.removed, pass.voided, pass.redeemed (fires when a pass is scanned and accepted at a scanner device), template.republished, or * for all pass events (template.republished must be subscribed by name).

curl https://api.passmint.com/v1/webhooks \
  -H "Authorization: Bearer pmk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/passmint",
    "events": ["pass.issued", "pass.added_to_wallet"],
    "description": "Production webhook"
  }'
{
  "object": "webhook",
  "id": "whk_xxx",
  "url": "https://example.com/webhooks/passmint",
  "events": ["pass.issued", "pass.added_to_wallet"],
  "description": "Production webhook",
  "enabled": true,
  "secret": "whsec_xxx",
  "created_at": "2026-04-16T12:00:00Z"
}
GET/v1/webhooks

List webhooks

Returns all webhooks for your account. Send limit or starting_after to page through them instead.

limitintegerNumber of results (default 100, max 100).
starting_afterstringCursor: the id of the last item on the previous page. See Pagination.
curl https://api.passmint.com/v1/webhooks \
  -H "Authorization: Bearer pmk_test_xxx"
GET/v1/webhooks/:id

Retrieve a webhook

Returns a single webhook by ID. The secret field is not included in retrieval responses.

curl https://api.passmint.com/v1/webhooks/whk_xxx \
  -H "Authorization: Bearer pmk_test_xxx"
PATCH/v1/webhooks/:id

Update a webhook

Update a webhook's URL, subscribed events, description, or enabled status. All fields are optional.

urlstringUpdated delivery URL.
eventsstring[]Updated event subscriptions.
descriptionstringUpdated description.
enabledbooleanEnable or disable the webhook.
curl -X PATCH https://api.passmint.com/v1/webhooks/whk_xxx \
  -H "Authorization: Bearer pmk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "events": ["*"], "description": "All pass events" }'
DELETE/v1/webhooks/:id

Delete a webhook

Permanently deletes a webhook and its delivery history. This is a hard delete and cannot be undone.

curl -X DELETE https://api.passmint.com/v1/webhooks/whk_xxx \
  -H "Authorization: Bearer pmk_test_xxx"
{
  "id": "whk_xxx",
  "deleted": true
}
GET/v1/webhooks/:id/deliveries

List recent deliveries

Returns delivery attempts for a webhook, newest first.

limitintegerNumber of results (default 100, max 100).
starting_afterstringCursor: the id of the last item on the previous page. See Pagination.
curl https://api.passmint.com/v1/webhooks/whk_xxx/deliveries \
  -H "Authorization: Bearer pmk_test_xxx"
{
  "object": "list",
  "data": [
    {
      "object": "webhook_delivery",
      "id": "whd_xxx",
      "webhook_id": "whk_xxx",
      "event_type": "pass.issued",
      "status": "delivered",
      "attempts": 1,
      "last_attempt_at": "2026-04-16T12:01:00Z",
      "next_attempt_at": null,
      "response_status": 200,
      "response_body": "ok",
      "created_at": "2026-04-16T12:00:00Z"
    }
  ],
  "has_more": false
}

Webhook object

idstringUnique identifier (whk_xxx).
objectstringAlways "webhook".
urlstringDelivery URL.
eventsstring[]Subscribed event types.
descriptionstring | nullOptional description.
enabledbooleanWhether the webhook is active.
created_atstringISO 8601 creation timestamp.

WebhookDelivery object

idstringUnique identifier (whd_xxx).
objectstringAlways "webhook_delivery".
webhook_idstringParent webhook (whk_xxx).
event_typestringThe event type that triggered delivery.
statusstringOne of: pending, delivered, failed, dead.
attemptsintegerNumber of delivery attempts.
last_attempt_atstring | nullISO 8601 timestamp of last attempt.
next_attempt_atstring | nullISO 8601 timestamp of next retry.
response_statusinteger | nullHTTP status from your server.
response_bodystring | nullResponse body from your server.
created_atstringISO 8601 creation timestamp.