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

For the other webhook endpoints, see the [Webhooks API reference](https://docs.usebila.com/docs/api-reference/webhooks/create-a-webhook-config.md).

> [!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.

***

## 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]
>
> `transfer.completed` fires at **initiation** with status `PROCESSING` - not at final settlement. Always subscribe to `payment.completed` and `payment.failed` for the definitive outcome.

***

## 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 attempts delivery 7 times: immediately, then after approximately 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours. Each delay is jittered by ±20%. The delivery is marked `FAILED` only after the seventh attempt fails.

Return `HTTP 200` in under 10 seconds 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);
}
```

> [!NOTE]
>
> Read the **raw request body as a string before JSON parsing** - the signature is computed against the raw bytes. Parsing first will break verification.

***

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


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