> ## 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.

# Webhook verification

> Verify TapAce webhook signatures using the original request body, handle verification errors, and deduplicate events before processing wallet lifecycle changes.

Use `tapace.webhooks.constructEvent()` to verify TapAce webhook signatures before processing an event. Keep the original request body bytes; parsing and reserializing JSON can invalidate a signature.

In the dashboard, select the intended organization and test or live mode, then open **Developers → Webhooks → Add endpoint**. Use a public HTTPS URL and select `pass.installed`, `pass.removed` or **All events**. Open the endpoint's **Settings → Reveal secret** and store it as `TAPACE_WEBHOOK_SECRET` in your receiver's environment. The signing secret is separate from your API key. See the [webhook guide](https://tapace.lk/docs/guides/webhooks.md).

## Verify an incoming request

The SDK accepts a string, Buffer or Uint8Array body and either a Headers object or a header record. It supports both Standard Webhooks names (`webhook-id`, `webhook-timestamp`, `webhook-signature`) and the corresponding `svix-*` names.

```typescript theme={null}
import {
  WebhooksResource, TapAceWebhookSignatureError,
} from "@tapace/node";

const webhooks = new WebhooksResource();

export function verifyTapAceWebhook(
  rawBody: Buffer,
  headers: Record<string, string | string[] | undefined>,
) {
  const secret = process.env.TAPACE_WEBHOOK_SECRET;
  if (!secret) throw new Error("Set TAPACE_WEBHOOK_SECRET");

  try {
    return webhooks.constructEvent(rawBody, headers, secret);
  } catch (error) {
    if (error instanceof TapAceWebhookSignatureError) {
      // Your HTTP handler should return 400.
      throw error;
    }
    throw error;
  }
}
```

If you already have a TapAce client, use `tapace.webhooks.constructEvent(rawBody, headers, secret)` instead. Standalone `WebhooksResource` does not require an API key.

## Express verification example

Express is your application's dependency, not a dependency of the SDK. This example verifies and prints events; replace the console output with durable acceptance before using it for business processing.

```javascript theme={null}
import express from "express";
import {
  WebhooksResource, TapAceWebhookSignatureError,
} from "@tapace/node";

const secret = process.env.TAPACE_WEBHOOK_SECRET;
if (!secret) throw new Error("Set TAPACE_WEBHOOK_SECRET");

const app = express();
const webhooks = new WebhooksResource();

// Register this route before any global express.json() middleware.
app.post("/webhooks/tapace", express.raw({ type: "application/json" }), (req, res) => {
  try {
    const event = webhooks.constructEvent(req.body, req.headers, secret);
    console.log(event.id, event.type);
    res.sendStatus(204);
  } catch (error) {
    if (error instanceof TapAceWebhookSignatureError) {
      res.sendStatus(400);
      return;
    }
    res.sendStatus(500);
  }
});

app.listen(4100);
```

Expose the handler through your own public HTTPS endpoint when testing TapAce deliveries. A local port alone is not reachable by the service.

## Timestamp and signature checks

Default timestamp tolerance is 300 seconds. An older timestamp or one more than 300 seconds in the future is rejected. The optional fourth argument overrides the tolerance in seconds; keep your server clock synchronized rather than widening it to hide clock errors.

Timestamps must be decimal integer Unix seconds within JavaScript's safe integer range. Signatures and signing secrets must contain valid base64; malformed encodings and extra padding are rejected. Signing secrets may use the `whsec_` prefix and URL-safe base64.

The SDK accepts multiple version-1 signatures and verifies if one matches the configured secret, which supports deliveries signed during secret rotation. Update the receiver to the new signing secret when you rotate it. Missing headers, invalid signatures, out-of-window timestamps and invalid JSON raise `TapAceWebhookSignatureError`.

## Process events durably

After verification, persist or enqueue the event durably before returning a success response. If accepting it fails, return an error so delivery can retry. Perform longer work in a worker.

The verifier accepts repeated valid deliveries; it does not remember processed events. Use `event.id` to deduplicate deliveries in your own persistent storage. Coordinate the unique event record and business changes transactionally, or make each side effect independently idempotent. The current event types are `pass.installed` and `pass.removed`. Fetch details with `tapace.passes.retrieve(event.data.pass.id)` when needed; event data contains no holder details.

Webhook payload properties such as `created_at` and `data.pass.template_id` use their documented names. Verification does not convert them to camelCase or perform full runtime schema validation.

See [Webhooks](https://tapace.lk/docs/guides/webhooks.md) for platform reliability, delivery retries and presence snapshots.

## Test real lifecycle delivery

1. Create an endpoint in your test organization and configure its secret in the receiver. A local receiver needs a temporary HTTPS tunnel or another publicly reachable host.
2. Issue a dedicated test pass and install it on a phone. Verify the received `pass.installed` event, its `data.pass.id`, `livemode: false` and presence snapshot.
3. Remove that same pass from the wallet. Verify `pass.removed`. On Apple, the active device count decreases; it reaches zero when its last registered device removes it.
4. Reinstall the pass. Verify a new `pass.installed` event with a new `event.id`. Reinstallation is a new lifecycle event, not a duplicate of the first installation.
5. In the endpoint's **Deliveries**, open a delivery and select **Resend**. The repeated event must pass signature verification and be recognized using the existing `event.id`, without repeating your business change.
6. Send an unsigned request or change the body without updating its signature. Your receiver must reject it.
7. Disable or delete a temporary endpoint before stopping its tunnel, so TapAce does not keep retrying an offline URL.

Apple events report individual device registration changes. Google lifecycle events are best effort. Use the current pass's `presence` when you need to reconcile installation state; do not infer it from a notification's delivery status.


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