Webhooks
Signed, retried, replayable event delivery.
Event families
provider_connection.*, provider_model_sync.*, tenant.*, agent.*, installation.*, knowledge_source.*, connection.*, session.*, run.* (+ subagent.*), action.*, usage.*. Subscribe with POST /v1/webhooks (secret-key only); filter by family. Inspect the delivery log at GET /v1/webhook-deliveries and replay any delivery with POST .../:id/replay.
Verification
Every delivery carries X-Webhook-Signature: sha256=<hex>, X-Webhook-Event, and X-Webhook-Timestamp. The signature is HMAC-SHA256 over the exact JSON body with your endpoint secret:
import crypto from 'crypto';
function verifyWebhook(
rawBody: string,
signature: string | null | undefined,
secret: string,
): boolean {
if (!signature || !/^sha256=[0-9a-f]{64}$/i.test(signature)) return false;
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const receivedBuffer = Buffer.from(signature, 'utf8');
const expectedBuffer = Buffer.from(expected, 'utf8');
return receivedBuffer.length === expectedBuffer.length
&& crypto.timingSafeEqual(receivedBuffer, expectedBuffer);
}Always verify on the raw body bytes before parsing — parsed-then-restringified JSON can differ in whitespace and fail the check. Treat missing, malformed, or incorrectly sized signatures as an authentication failure; verification should return false, never throw.
The HMAC covers the body, not X-Webhook-Timestamp. Do not treat the header alone as authenticated proof of freshness. After verifying the signature, parse the signed body's timestamp, reject events outside your allowed time window, and deduplicate by the signed data.event_id when present. Keep the deduplication ledger longer than your accepted time window. The header is only a convenience copy of the body timestamp.
Delivery guarantees
At-least-once with exponential backoff (2s, 4s, 8s) and 24-hour retry budget. 4xx (other than 429) is non-retryable; 5xx, 429, and network errors retry. Design every handler idempotently: deliveries (and replays) can arrive more than once. Payloads carry their own api_version so additive changes never break your parser.