Webhooks.
Get notified in real time when passes are installed, updated, or removed from a holder's wallet. Webhooks let you react to lifecycle events without polling. Just point us at a URL and we'll deliver signed payloads as they happen.
Event types
Every webhook delivery includes a type field identifying the event. Subscribe to the ones you care about, or use "*" to receive all of the pass events.
template.republished is the exception: it is sent only to endpoints subscribed to it by name, and "*" does not include it. Its payload has data.object.template_republish and no pass, and GET /v1/events does not list it. The dashboard leaves it unticked by default when you add an endpoint.
pass.issued: A new pass was signed and issued.pass.add_intent: A holder took a step toward adding the pass, either downloading the file or tapping Add to Google Wallet.pass.added_to_wallet: The pass was added to a holder's Apple Wallet or Google Wallet.pass.update_pushed: A pass's field values changed and the update reached at least one installed wallet. Not sent when no wallet holds the pass or every push failed.pass.update_delivered: A device pulled the updated pass (Apple-confirmed delivery).pass.update_failed: An update or void was sent and a wallet platform refused it: Apple rejected a push, or the Google Wallet object could not be patched. data.object.delivery_failure says which platform and why. Not sent when no wallet holds the pass.pass.removed: The pass was removed from a holder's wallet.pass.voided: The pass was voided.pass.redeemed: The pass was scanned and accepted at a scanner device. Rejected scans do not fire this event.template.republished: A template republish finished: every pass it reached has the new design, and data.object.template_republish has the counts. Sent only to endpoints subscribed to this event by name, not to *.
Creating a webhook
Register an endpoint with POST /v1/webhooks. Provide a url (required) and an events array (required). You can optionally include a description and an enabled flag (defaults to true). Use "*" in the events array to subscribe to all pass event types (not template.republished).
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.added_to_wallet", "pass.removed"],
"description": "Track wallet installs"
}'The signing secret (whsec_xxx) is returned only on creation: store it immediately. You will not be able to retrieve it again.
Payload format
Every delivery sends a JSON body with the event metadata and the affected pass:
{
"id": "psEvnt_abc123",
"object": "event",
"type": "pass.added_to_wallet",
"api_version": "2026-06-26",
"created_at": "2026-04-16T12:00:00.000Z",
"idempotency_key": "aapl:registered:pass_xxx:dlh_3f2a...:v1",
"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": null,
"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
}data.object.pass is a snapshot of the pass when the event was sent. Holder details are minimized to an email hash and a name-present flag; fetch GET /v1/passes/:id for the full pass. image_variant is the template image variant the pass renders with, or nullfor the template's own images (see Pass images). previous_attributes is always null for now.
pass.update_failed fires when an update or void was attempted and a wallet platform refused it: Apple Wallet pushes refused for a reason other than the holder deleting the pass, or a Google Wallet update error. When the failure may clear by itself, Passmint retries first and the event fires only if the retries run out (about 38 hours); failures that will not clear fire straight away. It does not fire when there was nobody to tell, such as a pass with no installed devices or one not saved to Google Wallet. You get one event per failing platform, and the change itself is still saved. It adds data.object.delivery_failure, and subscriptions to "*" receive it:
{
"type": "pass.update_failed",
...
"data": {
"object": {
"pass": { "id": "pass_xxx", ... },
"delivery_failure": {
"platform": "apple",
"reason": "403 InvalidProviderToken",
"failed": 1,
"devices": 2,
"context": "update"
}
}
}
}platform is apple or google. failed counts refused pushes (a number) and devices counts installed devices. devices is null for Google, and when an Apple pass could not be re-signed on a void. context is update or void.
Headers sent with each delivery:
Content-Type: application/jsonpassmint-signature: t=<unix_timestamp>,v1=<hmac_sha256>passmint-event-type: pass.added_to_walletUser-Agent: passmint-webhooks/1.0
Verifying signatures
Every delivery is signed with HMAC-SHA256 using the webhook secret returned at creation time. The passmint-signature header contains a timestamp and signature you should verify before trusting the payload.
The Node SDK handles this for you via webhooks.constructEvent:
import { Passmint } from "@passmint/node"
const passmint = new Passmint({
apiKey: process.env.PASSMINT_API_KEY!,
})
// In your webhook handler
const event = passmint.webhooks.constructEvent(
rawBody,
request.headers["passmint-signature"]!,
process.env.PASSMINT_WEBHOOK_SECRET!,
)
switch (event.type) {
case "pass.added_to_wallet":
// Handle install
break
case "pass.removed":
// Handle removal
break
}The default tolerance is 5 minutes. Signatures older than that are rejected to prevent replay attacks.
Retries
Failed deliveries are retried up to 7 times with exponential backoff:
- Immediate
- 30 seconds
- 5 minutes
- 30 minutes
- 2 hours
- 12 hours
- 24 hours
After 7 failed attempts the delivery status becomes dead. Your endpoint should return a 2xx status within 10 seconds: anything else is treated as a failure.
Managing webhooks
Full CRUD is available on the /v1/webhooks resource:
GET /v1/webhooks: list all webhooksGET /v1/webhooks/:id: retrieve a single webhookPATCH /v1/webhooks/:id: updateurl,events,enabled, ordescriptionDELETE /v1/webhooks/:id: permanently delete a webhook and its delivery history
Monitoring deliveries
Inspect recent delivery attempts for any webhook with GET /v1/webhooks/:id/deliveries. Each delivery record includes the HTTP status, response body (truncated to 2,000 characters), and timestamps, which helps when debugging failing endpoints.
Delivery statuses:
pending: queued, not yet attempteddelivered: your endpoint returned a 2xxfailed: attempt failed, will be retrieddead: all retry attempts exhausted