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

## 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"]
  }'
```

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`
* `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`

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

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


## Related topics

- [Test mode](/fundamentals/test-mode.md)
- [Delete a webhook](/api-reference/webhooks/delete-a-webhook.md)
- [Get a webhook](/api-reference/webhooks/get-a-webhook.md)
- [List webhooks](/api-reference/webhooks/list-webhooks.md)
- [Update a webhook](/api-reference/webhooks/update-a-webhook.md)
