Webhooks
Events sent to your server, and how to verify them.
Webhooks send a POST to your server when something happens on a project, so you do not have to poll. Add endpoints under Webhooks; each one chooses its events and gets its own signing secret, shown once.
Events
| Event | When |
|---|---|
balance.low | The wallet fell below your low-balance threshold (at most once per hour). |
spend_cap.warning | A key or project spend cap reached 80% (once per cap per period). |
payment.succeeded / payment.failed | A top-up was paid and credited, or failed. |
invoice.issued | A tax invoice was issued for a top-up. |
key.created / key.revoked | An API key was created or revoked. The key itself is never sent. |
project.archived | The project was archived. |
Payload
{
"id": "tb_whd_…",
"event": "balance.low",
"created_at": "2026-10-02T07:05:30.000Z",
"data": { "project_id": "tb_prj_…", "available": "48.250000", "threshold": "50.00", "currency": "INR" }
}
Amounts are decimal strings in INR. Respond with any 2xx within 10 seconds; anything else is retried with exponential backoff, and an endpoint that keeps failing is disabled (you can resume it from the dashboard).
Verifying signatures
Deliveries follow Standard Webhooks: headers webhook-id, webhook-timestamp and webhook-signature (v1,<base64 HMAC-SHA256> over id.timestamp.body, keyed with the base64 part of your whsec_ secret). Use the official standardwebhooks library, or verify by hand:
import crypto from 'node:crypto';
function verify(secret, headers, body) {
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
const signed = `${headers['webhook-id']}.${headers['webhook-timestamp']}.${body}`;
const expected = 'v1,' + crypto.createHmac('sha256', key).update(signed).digest('base64');
const ok = headers['webhook-signature'].split(' ').some((s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
const fresh = Math.abs(Date.now() / 1000 - Number(headers['webhook-timestamp'])) < 300;
return ok && fresh;
}
Verify against the raw request body, before parsing it. Rotating a secret from the dashboard makes the old one stop verifying immediately.