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

# Webhooks

> Receive crypto payment lifecycle events on your server.

# Webhooks

WakaPay can POST events to your HTTPS endpoint when crypto checkout payments are created, paid, or expired. Configure webhook URLs per organisation in the [dashboard](https://account.wakapay.cash).

## Headers

| Header                | Example            | Notes                               |
| --------------------- | ------------------ | ----------------------------------- |
| `Content-Type`        | `application/json` | Always JSON                         |
| `X-Wakapay-Event`     | `payment.paid`     | Also present in the body as `event` |
| `X-Wakapay-Timestamp` | `1724500000000`    | Unix time in milliseconds           |
| `X-Wakapay-Signature` | hex HMAC           | See verification below              |

Events: `payment.created` | `payment.paid` | `payment.expired`

## Verify the signature

```
expected = HMAC-SHA256(webhook_secret, "{timestamp}.{rawBody}").digest("hex")
```

1. Read the **raw HTTP body bytes** (do not re-serialize JSON)
2. Concatenate `X-Wakapay-Timestamp` + `"."` + raw body
3. HMAC-SHA256 with your organisation webhook secret
4. Compare digests in constant time
5. Reject if the timestamp is outside a \~5 minute window

Return **2xx** when accepted. Return **4xx** for bad signatures so WakaPay does not keep retrying a request that will never succeed. Failed deliveries retry with backoff.

```javascript theme={null}
import crypto from 'crypto';
import express from 'express';

function verifyWakapayWebhook({ rawBody, timestamp, signature, secret }) {
  if (!timestamp || !signature) return false;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');
  const a = Buffer.from(signature, 'hex');
  const b = Buffer.from(expected, 'hex');
  if (a.length !== b.length) return false;
  if (!crypto.timingSafeEqual(a, b)) return false;
  return Math.abs(Date.now() - Number(timestamp)) < 5 * 60 * 1000;
}

app.post(
  '/webhooks/wakapay',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const ok = verifyWakapayWebhook({
      rawBody: req.body,
      timestamp: req.get('X-Wakapay-Timestamp'),
      signature: req.get('X-Wakapay-Signature'),
      secret: process.env.WAKAPAY_WEBHOOK_SECRET,
    });
    if (!ok) return res.status(401).json({ detail: 'Invalid signature' });
    const payload = JSON.parse(req.body.toString('utf8'));
    // handle payload.event — e.g. mark the order paid on payment.paid
    res.status(200).json({ status: 'ok' });
  },
);
```

## Payload shape

Body is always `{ "event": "...", "data": { ... } }`. Use `data.email` (and other fields in `data`) to reconcile against your own users.

## Related

* [Create a checkout session](/api-reference/create-a-checkout-session)
* [Checkout session status](/api-reference/get-checkout-session-status)
* [Authentication](/authentication)
* [wakapay.cash](https://wakapay.cash)
