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 |
node.capacity_changed | A connected node's receiving capacity became degraded or recovered |
Subscriptions can also include * to receive all of the above events.
node.capacity_changed
Fires when a node bound to your workspace can no longer receive payments (or recovers). The data payload:
{
"nodeId": "…",
"status": "degraded",
"reason": "no_channels",
"activeChannels": 0,
"totalChannels": 0,
"receivingCapacitySat": 0,
"totalCapacitySat": 0
}status is "degraded" or "ok" (recovery). On a degraded event, reason is one of no_channels, peers_offline, receiving_full, or receiving_low; on ok it is null. The degraded event is emitted at the same moment the capacity email is sent (after the same debounce), and a paired ok event follows once the node recovers.
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 |
With the SDK (recommended)
@bolthub/pay ships a verifier that does all of this for you: constant-time signature check, replay window (default 5 minutes), and typed errors. It returns the parsed event on success.
import { verifyWebhook, WebhookVerificationError } from "@bolthub/pay";
const event = verifyWebhook({
secret: process.env.BOLTHUB_WEBHOOK_SECRET,
payload: rawBody, // the RAW request body, string or bytes
signature: req.header("x-webhook-signature"),
timestamp: req.header("x-webhook-timestamp"),
});
// event.event === "invoice.settled", event.data = event-specific payloadEvery failure throws WebhookVerificationError with a code that names the exact check that failed: missing_secret, missing_signature, missing_timestamp, invalid_timestamp, timestamp_out_of_tolerance, invalid_signature_format, signature_mismatch, or invalid_payload_json. Respond with 400 and investigate; never process an unverified body.
The payload must be the raw bytes. If your framework parses JSON before your handler runs, re-serialized JSON will not match the signature. Capture the raw body first:
- Express:
app.post("/webhook", express.raw({ type: "application/json" }), handler)then passreq.body(aBuffer). - NestJS: create the app with
{ rawBody: true }and passreq.rawBody. - Fetch-style servers (Next.js route handlers, Bun, Workers):
const rawBody = await request.text().
Manual verification (other languages)
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. Compare with a constant-time function, and reject timestamps outside a short replay window (the SDK uses 5 minutes).
Example (Node.js):
import crypto from "node:crypto";
function verifyWebhookSignature(secret, rawBody, timestamp, signatureHex) {
const signed = `${timestamp}.${rawBody}`;
const expected = crypto.createHmac("sha256", secret).update(signed).digest("hex");
if (expected.length !== signatureHex.length) return false;
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHex));
}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.
Listing External APIs
How to list a self-hosted L402 API in the bolthub directory without using the bolthub gateway.
@bolthub/pay (Payments SDK)
The bolthub payments SDK, both sides of the sale. Charge agents for an MCP tool or HTTP endpoint in a few lines, and pay for tools and L402 APIs with budgeted clients. Built on L402 (Lightning).