API reference.
Base URL: https://api.passmint.com
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_xxxThe 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.
/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"
}
}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"
}
]
}
}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-e07fc1f90ae7Response 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.
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"
}
}Passes
Passes are the core resource. Each represents a single wallet pass issued from a template.
Issue a pass
Create a new pass from a template. Supports idempotency.
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"
}List passes
Returns a paginated list of passes.
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
}Retrieve a pass
Returns a single pass by ID.
curl https://api.passmint.com/v1/passes/pass_xxx \
-H "Authorization: Bearer pmk_test_xxx"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.
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": []
}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.
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": [],
...
}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.
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.
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 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"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.
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
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.
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
Pass object
Templates
Templates define the design and structure of your passes. Every pass is issued from a template.
Create a template
Create a new pass template. Supports idempotency.
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"
}List templates
Returns a list of templates. Archived templates are left out unless you ask for them.
curl "https://api.passmint.com/v1/templates?limit=10" \
-H "Authorization: Bearer pmk_test_xxx"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"Update a template
Update a template's name, design, or archived status. All fields are optional.
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" }'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
}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.
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.
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.
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 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
}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)\" }"
doneIssue 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
Events
Events track the lifecycle of passes, from creation through installation and removal.
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.
curl "https://api.passmint.com/v1/events?limit=50" \
-H "Authorization: Bearer pmk_test_xxx"List events for a pass
Returns events for a specific pass.
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
Event pass snapshot
Event types
Webhooks
Webhooks deliver real-time event notifications to your server. Passmint signs every payload so you can verify authenticity.
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.
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"
}List webhooks
Returns all webhooks for your account. Send limit or starting_after to page through them instead.
curl https://api.passmint.com/v1/webhooks \
-H "Authorization: Bearer pmk_test_xxx"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"Update a webhook
Update a webhook's URL, subscribed events, description, or enabled status. All fields are optional.
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 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
}List recent deliveries
Returns delivery attempts for a webhook, newest first.
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
}