> ## Documentation Index
> Fetch the complete documentation index at: https://developers.perkstar.co.uk/llms.txt
> Use this file to discover all available pages before exploring further.

# Receive webhooks

> Verify signed Perkstar events, handle retries, and reconcile missed deliveries.

Webhooks notify your server when loyalty activity changes. Use them for fast
updates, then run periodic reconciliation so one delayed delivery cannot leave
systems permanently out of sync.

<Info>
  Building a Zap with the official Perkstar app? Follow [Connect Perkstar to
  Zapier](/integrations/zapier/setup). Zapier creates, deletes, and recreates
  its own event-specific subscriptions; you do not need to paste a Catch Hook
  URL into Perkstar for the same trigger.
</Info>

## Create an endpoint

Create webhook configuration with a narrowly scoped live key or in the
dashboard. The plaintext signing secret is returned once.

```bash theme={null}
curl --request POST "$PERKSTAR_API_URL/webhooks" \
  --header "Authorization: Bearer $PERKSTAR_API_KEY" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: webhook-primary-v1" \
  --data '{
    "url": "https://integration.example.com/webhooks/perkstar",
    "events": ["transaction.created", "customer.enrolled"],
    "card_id": "card_123",
    "mode": "live"
  }'
```

Store the returned secret in the receiving service's secret manager before the
response is discarded.

Common event names include:

* `customer.enrolled`, `customer.unenrolled`, `customer.anonymized`,
  `customer.group_changed`
* `wallet.installed`, `card.scanned`, `card.expired`, `referral.created`
* `transaction.created`, `reward.redeemed`, `coupon.redeemed`, `tier.changed`
* `membership.purchased`, `membership.renewed`, `membership.cancelled`
* `booking.created`, `booking.attended`, `booking.no_show`, `booking.cancelled`
* `webhook.test`

The [complete event catalogue](/reference/webhook-events) documents every
supported event, its exact `data` fields, and an example payload. Subscribe
only to events your receiver handles. An empty `events` array subscribes the
endpoint to all events. `card_id` is optional and excludes events for every
other card before delivery; events without a matching card are also excluded.
`mode` accepts `live`, `test`, or `all` and defaults to `all` for backward
compatibility. These filters avoid creating delivery rows or outbound requests
that the receiver would discard.

<Note>
  Some merchant events represent an observed state rather than a synchronous API
  write. Recently issued Google Wallet passes are eligible for observation on a
  15-minute schedule, but provider cadence, object age, and projector backlog
  can extend the delay. Card scans cover successful online Perkstar Scanner
  lookups only. Card expiry is per enrolment and effective expiry instant, and
  lifecycle-group changes do not represent membership in saved segments. See
  each event's boundary in the catalogue before designing a workflow.
</Note>

<Warning>
  When a business enables its first endpoint for `customer.group_changed` or
  Google `wallet.installed`, allow one completed projection sweep for Perkstar
  to store the current state without replaying historical customers or installs.
  Tracking starts after that warm-up baseline. Apple installation is
  callback-driven and does not use this warm-up.
</Warning>

## Event envelope

Every request body uses the same envelope:

```json theme={null}
{
  "id": "whd_01JEXAMPLE",
  "type": "transaction.created",
  "created": 1785312000,
  "orgId": "org_123",
  "data": {
    "transactionId": "txn_123",
    "enrollmentId": "enr_123",
    "type": "STAMP",
    "delta": 1,
    "occurredAt": "2026-07-30T10:00:00.000Z"
  }
}
```

`id` is stable across automatic retries. For business events, `created` is the
source event's `data.occurredAt` time expressed as Unix seconds, so it also
remains stable. Deduplicate with `id`; do not use either timestamp as an
identity key.

Perkstar also sends:

| Header | Purpose |
| - | - |
| `X-Perkstar-Event` | Dotted event name |
| `X-Perkstar-Webhook-Id` | Configured endpoint ID |
| `X-Perkstar-Delivery-Id` | Stable logical delivery ID; matches body `id` |
| `X-Perkstar-Timestamp` | Unix time for this signing and delivery attempt |
| `X-Perkstar-Signature` | `t=...,v1=...` HMAC signature |

For a complete runnable test receiver, persistence example and expected-result
checklist, follow [Your first safe POS test](/quickstart#4-receive-a-signed-test-event).

## Verify before parsing

Perkstar sends a Stripe-style signature:

```http theme={null}
X-Perkstar-Signature: t=1785312000,v1=<hex-hmac-sha256>
```

The signed value is `<timestamp>.<raw request body>`. Verify the raw bytes and a
five-minute timestamp tolerance before trusting or parsing the JSON.

```ts theme={null}
import { verifyWebhookSignature } from "@perkstar/api-client/webhooks";

const rawBody = await request.text();
const event = verifyWebhookSignature({
  payload: rawBody,
  signature: request.headers.get("x-perkstar-signature") ?? "",
  secret: process.env.PERKSTAR_WEBHOOK_SECRET!,
});

await processOnce(event.id, event);
return new Response(null, { status: 200 });
```

<Warning>
  Reading JSON first and serialising it again changes the signed bytes. Capture
  the raw body before any framework body parser runs.
</Warning>

## Processing rules

1. Verify the signature and timestamp.
2. Deduplicate by event ID in your database.
3. Persist or enqueue the event before returning `2xx`.
4. Process asynchronously when work could exceed a few seconds.
5. Make handlers safe when events arrive twice or out of order.
6. Branch test traffic using `testMode: true`.

Return a non-`2xx` status only when a retry may help. Validation failures should
be recorded for operator review rather than retried forever.

## Delivery schedule

Perkstar considers any `2xx` response successful. Network errors, timeouts, and
non-`2xx` responses are retried on this schedule:

| Attempt | Timing |
| - | - |
| 1 | Immediately |
| 2 | 1 minute after the first failure |
| 3 | 5 minutes after the second failure |
| 4 | 30 minutes after the third failure |

After four failed attempts, the delivery is marked exhausted and remains
available in the dashboard for inspection and manual resend. Keep each request
under ten seconds and acknowledge after durable persistence, before expensive
downstream work.

HTTP `410 Gone` is the exception to the retry schedule. Perkstar treats it as a
permanently removed callback, exhausts the current delivery immediately, and
pauses the endpoint so future events are not sent to a dead destination.

Automatic retries keep the same logical delivery `id`. A dashboard **Resend**
preserves that same logical ID. A receiver that has already committed the event
should acknowledge the duplicate without repeating its business action. If your
own downstream processing failed after receipt, retry it from your durable
inbox; a Perkstar resend must not bypass your deduplication protection.

## Test and operate the endpoint

In **Dashboard → Settings → Webhooks**, open the endpoint to:

* send a targeted `webhook.test` event while the endpoint is active;
* inspect attempt status, response code, duration, and the retained response
  excerpt;
* resend a failed or exhausted delivery after fixing the receiver;
* pause the endpoint without deleting its configuration;
* rotate the signing secret and update the receiver immediately; or
* remove the endpoint and its delivery history.

Production endpoint URLs must use HTTPS and resolve only to public network
addresses. Perkstar blocks localhost, private/reserved IP ranges, and DNS
rebinding targets.

## Recent event feed

`GET /events` returns one canonical source event regardless of how many
endpoints received it. It requires the dedicated `EVENTS_READ` permission,
defaults to live records, supports dotted event, card, customer, time, and
live/test filters, and retains rows for 90 days. This permission is separate
from `WEBHOOKS_READ` so an existing key that can inspect endpoint configuration
does not automatically gain customer event payload access.

Use the feed for setup samples and bounded reconciliation. It is not a polling
replacement for an instant subscription and it does not reveal signing secrets
or internal event-deduplication keys.

## Reconciliation

At least daily, compare the external system with recent Perkstar transactions
or customers. Webhooks provide speed, while reconciliation catches exhausted
retries, downtime, and deployment mistakes.


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