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.
POST body
Raw UTF-8 JSON
Header
Toolbench-Signature
HMAC
whsec_… key
Compare
timing-safe
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_secretfromPOST /webhooks, including thewhsec_prefix. - Reject timestamps more than five minutes from your server clock.
- Compare
v1with 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.