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.
Register a Webhook
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"
]
}'
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 |
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.
| 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
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);
}
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