Integration recipe

Verify Toolbench webhook signatures

Read the raw body, parse Toolbench-Signature (t and v1), HMAC-SHA256 the string t + '.' + rawBody with the full whsec_… secret, then compare v1 in constant time and reject timestamps older than five minutes.

Node and Python examples with constant-time compare and timestamp tolerance.

Rules

  • Header: Toolbench-Signature: t=<unix>,v1=<hex>
  • Signed message: ${t}.${rawBody} — use the raw request body string, not parsed JSON.
  • Key: the full signing_secret from POST /webhooks, including the whsec_ prefix.
  • Reject timestamps more than five minutes from your server clock.
  • Compare v1 with a constant-time function.
import { createHmac, timingSafeEqual } from "node:crypto";

/** secret is the full whsec_… value from POST /webhooks (prefix included). */
export function verifyToolbenchWebhook(rawBody, header, secret, { maxAgeSec = 300, nowSec = Math.floor(Date.now() / 1000) } = {}) {
  let timestampSec = null;
  let signature = null;
  for (const part of header.split(",").map((p) => p.trim())) {
    const eq = part.indexOf("=");
    if (eq === -1) continue;
    const k = part.slice(0, eq);
    const v = part.slice(eq + 1);
    if (k === "t" && v) timestampSec = Number(v);
    if (k === "v1" && v) signature = v;
  }
  if (!timestampSec || !signature || !Number.isFinite(timestampSec)) return false;
  if (Math.abs(nowSec - timestampSec) > maxAgeSec) return false;
  const expected = createHmac("sha256", secret)
    .update(`${timestampSec}.${rawBody}`, "utf8")
    .digest("hex");
  try {
    const a = Buffer.from(expected, "utf8");
    const b = Buffer.from(signature, "utf8");
    return a.length === b.length && timingSafeEqual(a, b);
  } catch {
    return false;
  }
}
Delivery
Events usually arrive within about one minute. Failed deliveries retry with backoff. URLs must be public HTTPS — private IPs are rejected and redirects are not followed.

API reference: Webhooks guide and POST /webhooks.