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

EventWhen
balance.lowThe wallet fell below your low-balance threshold (at most once per hour).
spend_cap.warningA key or project spend cap reached 80% (once per cap per period).
payment.succeeded / payment.failedA top-up was paid and credited, or failed.
invoice.issuedA tax invoice was issued for a top-up.
key.created / key.revokedAn API key was created or revoked. The key itself is never sent.
project.archivedThe project was archived.

Payload

json
{
  "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:

javascript
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.