> ## Documentation Index
> Fetch the complete documentation index at: https://tapace.lk/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Get notified when holders add or remove passes.

TapAce sends an HTTPS `POST` to your server when something happens to a pass, so you don't have to poll.

## Events

| Event | Sent when |
| - | - |
| `pass.installed` | A pass was added to a wallet. Apple reports each device that adds it. Google reports saves on a best-effort basis. |
| `pass.removed` | A pass left a wallet. Apple reports each device that removes it, or a device that stops accepting updates. Google reports deletions on a best-effort basis. |

## Add an endpoint

1. In the dashboard, open **Developers → Webhooks**. Endpoints are separate for test and live, so pick the right mode first.
2. Select **Add endpoint** and enter your URL. It must be a public `https://` address.
3. Choose the events to receive, or **All events** to also get event types added later.
4. Copy the endpoint's **signing secret**. You'll use it to verify each request.

Use **Send test event** on the endpoint's page to check your server before real events arrive.

## Payload

Every event has the same shape. It contains no holder details, so [fetch the pass](https://tapace.lk/docs/api-reference/passes/get.md) by `data.pass.id` if you need them.

```json theme={null}
{
  "id": "evt_Hn4Lq9Xv2Mt7Rk8Pz3Wc",
  "object": "event",
  "type": "pass.installed",
  "created_at": "2026-10-13T10:05:12.000Z",
  "livemode": false,
  "data": {
    "pass": {
      "id": "pass_7hGk2LmQ9xVt4RnB8cYe",
      "short_id": "6BkJLsso1xyB",
      "template_id": "tmpl_Q4vN8xT2mLr7KpZs3Wd1",
      "status": "active"
    },
    "platform": "apple",
    "reliability": "exact",
    "source": "device_registered",
    "installation_id": "areg_Tz8Kp3Vn6Lq2Xm9Rc4Wb",
    "occurred_at": "2026-10-13T10:05:11.000Z",
    "presence": {
      "apple": { "active_devices": 1 },
      "google": { "observed_state": "unknown" }
    }
  }
}
```

**id** `string`

Unique event ID. A delivery can arrive more than once, so use it to skip duplicates.

**livemode** `boolean`

`true` for live passes, `false` for test passes.

**data.platform** `string`

`apple` or `google`.

**data.reliability** `string`

`exact` for Apple, which reports every device. `best_effort` for Google, which may report late or not at all.

**data.source** `string`

What triggered the event. For `pass.installed`: `device_registered` or `google_saved`. For `pass.removed`: `device_unregistered`, `push_token_rejected` or `google_deleted`.

**data.installation\_id** `string | null`

Identifies one Apple device's copy of the pass and stays the same if it's re-added. `null` for Google.

**data.presence** `object`

Where the pass is now. `apple.active_devices` counts devices that have it. `google.observed_state` is `saved`, `removed` or `unknown`. `apple` or `google` is `null` when the pass wasn't issued for that wallet.

## Keep your records in sync

A webhook reports a change at the moment it happens. To check where a pass is at any time, for example after your server was down, [get the pass](https://tapace.lk/docs/api-reference/passes/get.md) and read its `presence`. It holds the same figures as `data.presence`: Apple's `activeDevices` and Google's `observedState`. [List passes](https://tapace.lk/docs/api-reference/passes/list.md) returns `presence` for every pass on the page.

## Verify signatures

For Node.js applications, the [TapAce SDK verifier](https://tapace.lk/docs/sdk/node/webhooks.md) accepts both the `svix-*` and Standard Webhooks `webhook-*` header names. It requires the original body bytes and the endpoint's signing secret. The SDK verifies signatures but does not persist or deduplicate events for you.

Each request carries `svix-id`, `svix-timestamp` and `svix-signature` headers. Verify them with the endpoint's signing secret before trusting the payload. The [Svix libraries](https://docs.svix.com/receiving/verifying-payloads/how) do this for you:

```javascript Node.js theme={null}
import { Webhook } from "svix";

const webhook = new Webhook(process.env.TAPACE_WEBHOOK_SECRET);

// rawBody must be the exact request body, before JSON parsing.
const event = webhook.verify(rawBody, {
  "svix-id": req.headers["svix-id"],
  "svix-timestamp": req.headers["svix-timestamp"],
  "svix-signature": req.headers["svix-signature"],
});
```

`verify` throws if the signature is wrong or the timestamp is too old. Reply with a `400` in that case.

## Respond and retry

Verify the signature and durably save or enqueue the event before replying with a `2xx` status. Return an error if durable acceptance fails. Run longer business processing in a worker after accepting the event. Any other response counts as a failure, and the delivery is retried automatically with growing delays. Each endpoint's page in the dashboard lists recent deliveries, and you can resend one from there.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.