Webhooks
Subscribe to tenant-scoped events, verify signed deliveries, and manage webhook subscriptions via the bolthub API.
Overview
Webhooks let your backend react to bolthub events in real time. Each subscription points at an HTTPS URL; bolthub POSTs JSON payloads and signs them with HMAC-SHA256 so you can verify authenticity.
All webhook management routes require authentication and are scoped to the tenant (tenantId in the path).
Event types
| Event | Description |
|---|---|
invoice.settled | Invoice was paid |
session.created | New session started |
session.expired | Session expired |
billing.cycle_closed | Billing cycle closed |
billing.payment_received | Billing payment received |
billing.suspended | Tenant suspended for non-payment |
endpoint.health_changed | Endpoint health status changed |
Subscriptions can also include * to receive all of the above events.
API routes
Base URL in production is https://api.bolthub.ai (use your local API host in development). Paths below are relative to that origin.
| Method | Path | Description |
|---|---|---|
GET | /tenants/:tenantId/webhooks | List webhook subscriptions |
POST | /tenants/:tenantId/webhooks | Create a webhook |
PATCH | /tenants/:tenantId/webhooks/:id | Update a webhook |
DELETE | /tenants/:tenantId/webhooks/:id | Delete a webhook |
GET | /tenants/:tenantId/webhooks/:id/deliveries | List delivery history for that subscription |
POST | /tenants/:tenantId/webhooks/deliveries/:deliveryId/retry | Retry a failed delivery |
POST | /tenants/:tenantId/webhooks/:id/test | Send a test event |
Create webhook
POST /tenants/:tenantId/webhooks
Body:
{
"url": "https://example.com/webhooks/bolthub",
"events": ["invoice.settled", "session.created"]
}urlmust use HTTPS and must not resolve to private or otherwise blocked addresses (SSRF protection).eventsmust contain at least one valid event name (or*).
Response includes the created webhook and a secret (shown only on create). Store the secret securely; you need it to verify signatures.
Update webhook
PATCH /tenants/:tenantId/webhooks/:id
Optional body fields: url, events, isActive. The secret is not rotated by this endpoint.
Delivery history
GET /tenants/:tenantId/webhooks/:id/deliveries
Optional query: limit (default 50, max 100). Returns recent deliveries with status, HTTP status code, attempts, errors, and timestamps.
Test delivery
POST /tenants/:tenantId/webhooks/:id/test
Dispatches a synthetic invoice.settled-shaped delivery so you can confirm connectivity and signature verification. The JSON body uses the same envelope as real events (see below).
Delivery format
bolthub sends an HTTP POST with Content-Type: application/json. Body shape:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"event": "invoice.settled",
"timestamp": "2025-03-23T12:00:00.000Z",
"data": { }
}id- unique delivery / event id (also sent asX-Webhook-Id)event- event typetimestamp- ISO 8601 time of the payloaddata- event-specific payload
Verifying signatures
Headers on each delivery:
| Header | Description |
|---|---|
X-Webhook-Id | Same as id in the JSON body |
X-Webhook-Signature | Hex-encoded HMAC-SHA256 of the signing string |
X-Webhook-Timestamp | Unix timestamp in milliseconds (string), same epoch used for signing |
X-Webhook-Event | Same as event in the JSON body |
Signing string: concatenate the timestamp, a single dot (.), and the raw JSON body string exactly as sent (byte-for-byte with the request body):
{timestamp}.{json_body}Compute HMAC-SHA256 using your webhook secret as the key; the signature bolthub sends is the hex digest of that HMAC.
Example (Node.js):
import crypto from "node:crypto";
function verifyWebhook(secret, rawBody, timestamp, signatureHex) {
const signed = `${timestamp}.${rawBody}`;
const expected = crypto.createHmac("sha256", secret).update(signed).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHex));
}Reject requests with stale timestamps if you want replay protection (not enforced by bolthub).
Retries and URL rules
- HTTPS only: HTTP URLs are rejected when creating or updating a webhook.
- Retries: After the first attempt, failed deliveries are retried 3 times with delays of 10 seconds, 1 minute, and 5 minutes (exponential-style backoff between attempts).
Error handling
Your endpoint should return a 2xx status to acknowledge success. Non-2xx responses are treated as failures and follow the retry policy. After retries are exhausted, the delivery is marked failed; use retry on the delivery record to try again manually.