Webhooks
Real-time HTTP notifications for invoice and payment events.
Billium uses webhooks to push event notifications to your server. Instead of polling the API to check whether an invoice was paid, you register an HTTPS endpoint and Billium calls it whenever something happens.
Anatomy of a webhook request
Every delivery is an HTTP POST with a JSON body and the following headers:
| Header | Description |
|---|---|
Content-Type | application/json |
x-signature | HMAC-SHA256 signature for verifying authenticity |
x-webhook-id | ID of the webhook configuration that triggered this delivery |
x-event-id | Unique ID of the event |
x-delivery-id | Unique ID of this specific delivery attempt |
Payload structure
Every event payload follows the same envelope:
{
"event": "invoice.paid",
"id": "evt_a1b2c3d4e5f6",
"data": { ... },
"timestamp": "2025-03-15T04:12:00.000Z"
}| Field | Description |
|---|---|
event | Event type string — see the event catalog. |
id | Unique event ID. Store it to deduplicate retries. |
data | Event-specific payload — different shape per event type. |
timestamp | ISO 8601 timestamp of when the event was emitted. |
Delivery behavior
- Your endpoint must return a
2xxstatus within the configured timeout (default 30 seconds, configurable per webhook). - On failure, Billium retries with exponential backoff starting at 60 seconds, up to 5 attempts total.
404,410, and451responses are treated as a signal to stop and move the delivery to the dead-letter queue immediately — no retries.- Other
4xxresponses (400,401,403, etc.) are treated as permanent failures and also skip retries. 5xxresponses and network timeouts are retried.- After all retries are exhausted, the delivery is moved to a dead-letter queue. Failed deliveries are retained for 30 days; successful deliveries for 7 days.
Make your webhook handler idempotent — Billium may deliver the same event more than once on retry. Use event.id to deduplicate.
Delivery guarantees
Not all events are delivered with the same guarantees. Billium classifies events into two categories:
| Class | Guarantee | Behavior |
|---|---|---|
| Durable | At-least-once. | Billium records the event atomically with the state change that caused it and retries delivery until your endpoint responds with a 2xx. Events survive Billium-side restarts and transient failures. Your handler will see each event at least once, possibly more — always deduplicate by event.id. |
| Best-effort | At-most-once. | Billium dispatches the event once as soon as it happens, without a retry log. If something goes wrong between the event and dispatch it may be dropped. Treat these as a performance optimization for live UI updates, not as a source of truth. |
Durable events (11): invoice.paid, invoice.underpaid, invoice.overpaid, invoice.expired, invoice.cancelled, payment.detected, payment.confirmed, payment.paid, payment.underpaid, payment.overpaid, payment.expired.
Best-effort events (4): invoice.created, invoice.updated, payment.created, payment.updated.
Drive your business logic off durable events only. Use invoice.paid / invoice.expired / invoice.cancelled (and the payment.* durables) as the source of truth for fulfillment, refunds, and accounting. Best-effort events are intended for checkout UI refreshes and merchant dashboards — they are fire-and-forget and may be dropped.
Signature verification
Each delivery includes an x-signature header. Verify it before processing the payload to ensure the request came from Billium and was not tampered with.
x-signature: t=1741406520,v1=a3f9...See Verify signatures for the full scheme and code examples, or use the @billium/node SDK which handles it for you.
Managing webhooks
You can create, update, and delete webhooks from the dashboard or with the @billium/node SDK. The REST endpoints are all under your merchant base path:
| Action | Endpoint |
|---|---|
| List webhooks | GET /api/v1/merchants/merchant/{merchantId}/webhooks |
| Create webhook | POST /api/v1/merchants/merchant/{merchantId}/webhooks |
| Update webhook | PATCH /api/v1/merchants/merchant/{merchantId}/webhooks/{webhookId} |
| Delete webhook | DELETE /api/v1/merchants/merchant/{merchantId}/webhooks/{webhookId} |
| Send test ping | POST /api/v1/merchants/merchant/{merchantId}/webhooks/{webhookId}/ping |
Webhook management endpoints require a secret key (sk_*). Public keys (pk_*) will be rejected.
The secret (whsec_...) is returned once, inside webhookSecrets[0].secretKeyPreview, at creation time. Store it immediately — the backend only keeps a hashed copy afterwards.