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

# Webhooks

> Register webhook endpoints and verify event signatures in the sandbox.

## Register a Webhook

```bash theme={null}
curl --request POST \
  --url https://sandbox.usebila.com/api/v1/bila/webhooks \
  --header 'Content-Type: application/json' \
  --header 'x-api-key: sk_test_your_api_key_here' \
  --data '{
    "url": "https://your-server.com/webhooks/bila",
    "events": [
      "payment.completed",
      "payment.failed",
      "withdrawal.completed",
      "withdrawal.failed",
      "order.paid"
    ]
  }'
```

<Warning>
  The full signing secret is returned **only at creation time**. Store it immediately — it is masked on all subsequent calls. If lost, rotate the secret to get a new one.
</Warning>

***

## Supported Events

| Event                  | When It Fires                                   |
| ---------------------- | ----------------------------------------------- |
| `payment.completed`    | Payment finished — **definitive final outcome** |
| `payment.failed`       | Payment failed — **definitive final outcome**   |
| `payment.created`      | Payment initiated                               |
| `withdrawal.created`   | Payout started                                  |
| `withdrawal.completed` | Payout succeeded                                |
| `withdrawal.failed`    | Payout failed                                   |
| `order.created`        | Customer placed a store order                   |
| `order.paid`           | Order paid                                      |
| `order.cancelled`      | Order cancelled                                 |
| `stock.low`            | Product stock at or below threshold             |
| `settlement.completed` | Settlement completed                            |

<Warning>
  `collection.completed` and `transfer.completed` fire at **initiation** with status `PROCESSING` — not at final settlement. Always subscribe to `payment.completed` and `payment.failed` for the definitive outcome.
</Warning>

***

## Delivery Headers

| Header             | What It Contains                               |
| ------------------ | ---------------------------------------------- |
| `X-Bila-Event`     | Event name — e.g. `payment.completed`          |
| `X-Bila-Delivery`  | Unique delivery ID — use to deduplicate events |
| `X-Bila-Timestamp` | Unix timestamp used for signing                |
| `X-Bila-Signature` | `sha256=<hex-digest>`                          |

Bila retries up to **3 times** on failure (\~5s, \~10s, \~20s). Return `HTTP 200` immediately and process asynchronously.

***

## Verify Signatures

```typescript theme={null}
import * as crypto from 'crypto';

function verifyBilaWebhook(
  rawBody: string,
  timestampHeader: string,
  signatureHeader: string,
  secret: string,
  maxAgeSeconds = 300,
): boolean {
  const timestamp = Number(timestampHeader);
  if (!Number.isFinite(timestamp)) return false;

  const age = Math.abs(Math.floor(Date.now() / 1000) - timestamp);
  if (age > maxAgeSeconds) return false;

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

  const expectedBuf = Buffer.from(expected);
  const receivedBuf = Buffer.from(signatureHeader);
  if (expectedBuf.length !== receivedBuf.length) return false;

  return crypto.timingSafeEqual(expectedBuf, receivedBuf);
}
```

<Info>
  Read the **raw request body as a string before JSON parsing** — the signature is computed against the raw bytes. Parsing first will break verification.
</Info>

***

## Pre-Launch Checklist

* Webhook registered and signing secret stored
* Signature verification implemented and tested
* `payment.completed` and `payment.failed` received correctly
* Duplicate delivery detection via `X-Bila-Delivery` working
* Endpoint returns `200` in under 10 seconds
