The signature is hex(HMAC_SHA256(secret, raw_request_body)).
Verify against the raw bytes of the body, before any JSON parsing, and use a
constant-time comparison. Re-serialising the parsed body will not reproduce the same bytes,
so the signature will not match.
import crypto from 'node:crypto';function verify(rawBody, signature, secret) { const expected = crypto.createHmac('sha256', secret).update(rawBody).digest(); const provided = Buffer.from(signature, 'hex'); // Check length first: timingSafeEqual throws if the buffers differ in size, // so a short or malformed signature would raise instead of returning false. return provided.length === expected.length && crypto.timingSafeEqual(provided, expected);}
Custom tool calls are signed the same way. An agent’s tools[] endpoints receive the same
X-Webhook-Signature header, the same secret, and the same scheme — so one verification helper
covers both webhooks and tool invocations.
Reference implementations for Node.js, Python, and Rust are in the repo’s webhook guide.
The follow-up workflow emits a fuller set, useful for building an SLA-aware queue:callback_task.updated, .dispatch_initiated, .dispatch_status,
.dispatch_status_updated, .completed, .reopened, .snooze_expired,
.follow_up_notification_sent, .follow_up_escalated, .human_takeover_required,
.sla_breached, .external_reconciled.
callback_task.sla_breached and .human_takeover_required are the two worth alerting on —
they signal work that will not complete without a person.
Deliveries are retried, and every POST carries a stable X-Idempotency-Key. Key your handler on
it so a retried delivery is a no-op rather than a duplicate.