Guide

Webhooks

Get told when your credits run low - before a call fails in production.

A prepaid API that only signals "you are out of credits" by failing a call has already broken something in your system. Webhooks let you find out while there is still time to top up.

Events

EventWhen it fires
credits.lowYour balance crosses below 25 credits. Fires once on the call that crosses it, not on every call after.
credits.exhaustedYour balance reaches zero. Calls are now being refused with 402.
credits.grantedCredits were added - purchase, monthly allowance or manual adjustment.
api_key.createdA key was minted on your account.
api_key.revokedA key was revoked.
webhook.testYou pressed Send test.

Setting one up

  1. Add an endpoint in your dashboard. The URL must be https (except localhost for testing).
  2. Copy the signing secret - like an API key, it is shown once and stored only as we need it.
  3. Press Send test and confirm your server returns a 2xx.

What we send

json
POST https://your-server.example.com/hooks/toolbox
X-ToolBox-Event: credits.low
X-ToolBox-Timestamp: 1700000000
X-ToolBox-Signature: sha256=9f86d081884c7d65...

{
  "id": "0f9b7c1e-...",
  "event": "credits.low",
  "createdAt": "2026-08-16T09:15:00+00:00",
  "data": {
    "balance": 18,
    "threshold": 25,
    "message": "Your credit balance is running low."
  }
}

Verifying the signature

Anyone who learns your webhook URL could post fake events at it, so verify every delivery before acting on it. The signature is HMAC-SHA256 over "{timestamp}.{body}" using your signing secret:

javascript
import crypto from "node:crypto";

function verify(req, secret) {
  const timestamp = req.headers["x-toolbox-timestamp"];
  const signature = req.headers["x-toolbox-signature"];
  const body = req.rawBody; // the RAW string, before JSON.parse

  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${body}`)
    .digest("hex");

  // Constant-time compare - a plain === leaks the correct signature by timing.
  const ok = crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));

  // Reject anything older than five minutes so a captured delivery cannot be replayed.
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  return ok && age < 300;
}

Delivery behaviour

  • Any 2xx counts as delivered. Everything else is a failure.
  • We wait up to 10 seconds. Return quickly and do your work asynchronously - a slow endpoint is treated as a failed one.
  • After 20 consecutive failures the endpoint is disabled automatically so we stop posting to a dead URL. Any single success resets the counter.
  • Every attempt - success or failure - is listed in your dashboard with the status code and duration.
  • A webhook failing never affects your API calls. Deliveries are dispatched separately from the call you paid for.