Webhooks

Get told when a pass is created, updated, installed or removed - setting up an endpoint, verifying the signature, handling retries, and what each event means.

Updated 7/26/2026

A webhook tells your systems when something happens to a pass, so you don't have to poll for it. The obvious use is knowing when someone actually installs a pass - the moment a lead becomes a pass holder.

Set them up under Settings → Webhooks.

The events

EventFires when
pass.createdA pass is created - by API, dashboard, or issuance form
pass.updatedA pass's details or status change
pass.installedSomeone adds the pass to Apple or Google Wallet
pass.removedSomeone deletes the pass from their wallet

Subscribe to the ones you need rather than all of them.

pass.removed is reliable on Apple Wallet. Google Wallet doesn't consistently report deletions, so treat removal counts as a lower bound rather than an exact figure.

Adding an endpoint

    1. Open Settings → Webhooks.
    2. Enter your endpoint URL - it must be publicly reachable and served over HTTPS.
    3. Tick the events you want.
    4. Add it.
    5. Copy the signing secret, shown once.

The payload

{
  "type": "pass.installed",
  "account_id": "...",
  "data": {
    "pass_id": "...",
    "external_id": "order-8891"
  }
}

external_id is your own reference, so you can match the event to your records without storing our ids. See External IDs and idempotency.

Three headers come with it:

  • X-Webhook-Event - the event type.
  • X-Webhook-Id - a stable id for this delivery.
  • X-Webhook-Signature - the signature you verify.

Verifying the signature

Anyone can POST to a public URL. The signature is how you know a request genuinely came from us.

It's an HMAC-SHA256 of the raw request body, keyed on your signing secret, hex-encoded.

import crypto from "node:crypto";

function verify(rawBody, signature, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature),
  );
}

Verify the signature on every request, and compute it over the raw body - the exact bytes received, before any JSON parsing. Parsing and re-serialising changes the bytes and the signature will never match.

Many frameworks parse the body for you by default. You usually have to opt out for the webhook route specifically.

Use a timing-safe comparison rather than ===.

Handle retries and duplicates

Delivery is at least once. A slow response, a timeout, or a redelivery means you can receive the same event twice.

Use X-Webhook-Id to deduplicate: record ids you've processed and ignore repeats. It's stable across retries of the same delivery.

Your handler should be idempotent regardless - processing an event twice shouldn't double anything.

Retry behaviour

If your endpoint doesn't return a 2xx, we retry up to six times with exponential backoff - roughly 30 seconds, then a minute, two, four, eight. After that the delivery is abandoned.

Requests time out after 10 seconds.

Do the minimum in the request and return 200 immediately. Queue the real work.

A handler that calls three other services before responding will eventually exceed 10 seconds, get retried, and do everything twice.

Testing

Point a subscription at a request-inspection service (RequestBin, webhook.site, or an ngrok tunnel to your machine) and issue a pass. You'll see the payload and headers exactly as we send them.

Test signature verification against a real delivery rather than a hand-built one - the raw-body detail is easy to get subtly wrong and only shows up against genuine traffic.

Rotating the secret

You can rotate a subscription's signing secret over the API by sending { "rotateSecret": true } to PATCH /api/v1/webhook-subscriptions/{id}. The new secret is returned once.

Rotate if the secret has been exposed, and deploy the new one promptly - deliveries are signed with the current secret, so verification fails until your side is updated.

Disabling versus deleting

Disable stops deliveries and keeps the endpoint and its secret - use it while your receiver is down for maintenance.

Delete removes it and destroys the secret. Re-adding gives you a new one, so your receiver needs updating.

Where to go next

More in Developers