> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tapace.lk/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](/api-reference/passes/get) 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" }
    }
  }
}
```

<ResponseField name="id" type="string">
  Unique event ID. A delivery can arrive more than once, so use it to skip duplicates.
</ResponseField>

<ResponseField name="livemode" type="boolean">
  `true` for live passes, `false` for test passes.
</ResponseField>

<ResponseField name="data.platform" type="string">
  `apple` or `google`.
</ResponseField>

<ResponseField name="data.reliability" type="string">
  `exact` for Apple, which reports every device. `best_effort` for Google, which may report late or not at all.
</ResponseField>

<ResponseField name="data.source" type="string">
  What triggered the event. For `pass.installed`: `device_registered` or `google_saved`. For `pass.removed`: `device_unregistered`, `push_token_rejected` or `google_deleted`.
</ResponseField>

<ResponseField name="data.installation_id" type="string | null">
  Identifies one Apple device's copy of the pass and stays the same if it's re-added. `null` for Google.
</ResponseField>

<ResponseField name="data.presence" type="object">
  Where the pass is now. `apple.active_devices` counts devices that have it. `google.observed_state` is `saved`, `removed` or `unknown`.
</ResponseField>

## Verify signatures

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

Reply with any `2xx` status within a few seconds, then do the work. 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.