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

# Webhooks

> Get shop events pushed to your endpoint, and verify they came from us.

Webhooks push events to a URL you control. Use them instead of polling — polling
an orders endpoint is the fastest way to exhaust a key's rate limit.

Set them up in **Growth → API**, or with a key holding `webhooks:manage`.

## Events

| Event              | Fires when                      |
| ------------------ | ------------------------------- |
| `order.created`    | An order is created             |
| `order.paid`       | Payment confirmed               |
| `order.fulfilled`  | Marked fulfilled                |
| `order.cancelled`  | Cancelled                       |
| `order.refunded`   | Refunded                        |
| `product.created`  | Product created                 |
| `product.updated`  | Product updated                 |
| `product.deleted`  | Product deleted                 |
| `customer.created` | Customer record created         |
| `customer.updated` | Customer record updated         |
| `inventory.low`    | Stock drops below the threshold |
| `review.created`   | Review submitted                |
| `review.approved`  | Review approved                 |

One endpoint can take all of them, or run several.

## The request

`POST` to your URL:

| Header                |                           |
| --------------------- | ------------------------- |
| `Content-Type`        | `application/json`        |
| `X-Webhook-Signature` | `t={timestamp},v1={hmac}` |
| `X-Webhook-Event`     | The event type            |
| `X-Webhook-ID`        | Unique delivery id        |

```json Body theme={"system"}
{
  "id": "delivery_abc123",
  "event": "order.paid",
  "created": "2026-08-20T10:30:00.000Z",
  "data": {
    "…": "event-specific payload"
  }
}
```

You have **30 seconds** to respond. Any `2xx` counts as success.

## Verify the signature

<Warning>
  Always verify. Your webhook URL is reachable by anyone who learns it, and an
  unverified handler will happily process a forged `order.paid`.
</Warning>

The signature is `t={unix_timestamp},v1={hex_hmac}`, where the HMAC is SHA-256
over `` `${timestamp}.${rawBody}` `` using your subscription secret (which
starts `whsec_`).

```js Node.js theme={"system"}
import crypto from 'crypto';

function verify(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(
    header.split(',').map(p => p.split('='))
  );
  const timestamp = parseInt(parts.t, 10);
  const received = parts.v1;
  if (!timestamp || !received) return false;

  // Reject anything outside the tolerance window, so a captured
  // request cannot be replayed later.
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > toleranceSeconds) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(received),
    Buffer.from(expected)
  );
}
```

<Warning>
  Sign against the **raw body**, byte for byte. If your framework parses JSON
  before you see it, re-serialising won't reproduce the same bytes and every
  signature will fail. Configure a raw-body handler for this route.
</Warning>

Tolerance defaults to 5 minutes.

## Delivery log

Every delivery records its status, HTTP code, duration and the first 1000
characters of your response body.

That response snippet is the most useful thing you'll have when debugging a
failing endpoint — so put a real error message in your non-2xx responses rather
than an empty body.

## Rotating the secret

You can regenerate it. Deliveries signed with the old one stop verifying
immediately, so deploy the new secret first, or accept a short window of
rejected deliveries.

## A good handler

<Steps>
  <Step title="Verify first">
    Before parsing, before any side effect.
  </Step>

  <Step title="Respond fast">
    Acknowledge with a 2xx, then do the work asynchronously.
  </Step>

  <Step title="Deduplicate on X-Webhook-ID">
    Treat it as an idempotency key. Assume you'll see one twice eventually.
  </Step>

  <Step title="Don't trust the payload for money decisions">
    Read back through the [Store API](/api/store/orders) before acting on
    anything financial.
  </Step>
</Steps>
