Skip to main content

Webhooks

Set a webhook URL and a signing secret on the project in the dashboard. The URL must be HTTPS on port 443 or 8443, without credentials, and not a private or internal host; each delivery times out after 5 seconds. Defraudo then POSTs signed JSON to the URL:

WebhookWhen
Decision deliveryFor every scored event with an allow, block or review verdict, sync or async. Body: event_id, risk_score, max_possible, decision, breakdown, flags, trust_override, hard_block_hit, latency_ms, environment, plus actions when non-empty.
Case resolutionAn analyst or API call decides a review case. Body: event_id, case_id, type, decision, reason, actor_ref, decided_at.
Case wokeA snoozed case returns to the open queue. Body: type, case_id, event_id, woke_at.

Return any 2xx to acknowledge. A non-2xx, a timeout or a redirect counts as failure and is retried with backoff, at most 15 minutes apart, until acknowledged. Delivery is at least once. Deduplicate decision webhooks on event_id. Case webhooks carry an internal event_id (or null) that is shared by the resolution and woke messages, so deduplicate those on message type + case_id + decided_at / woke_at. While a project has a URL but no secret, nothing is sent and held deliveries go out once a secret is set.

Verify the signature​

Each request has the header x-antifrod-signature: t=<unix-ms>,v1=<hex>[,v1=<hex>...]. The signature is HMAC-SHA256(secret, "<t>.<raw body>") in hex. Rules:

  • Use the raw request body, not re-serialised JSON.
  • Accept the request if any v1 matches, compared in constant time. During the 24 hours after a secret rotation the header carries signatures for the new secret and up to three recently replaced ones.
  • Reject if t is more than 5 minutes from your clock.
import crypto from 'node:crypto';

export function verify(rawBody, header, secret, maxSkewMs = 300_000) {
let t;
const signatures = [];

for (const part of header.split(',')) {
const [key, value] = [part.slice(0, part.indexOf('=')).trim(), part.slice(part.indexOf('=') + 1).trim()];
if (key === 't') t = parseInt(value, 10);
if (key === 'v1' && /^[0-9a-f]{64}$/i.test(value)) signatures.push(value);
}

if (!Number.isFinite(t) || signatures.length === 0) return false;
if (Math.abs(Date.now() - t) > maxSkewMs) return false;

const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();

return signatures.some((signature) => crypto.timingSafeEqual(Buffer.from(signature, 'hex'), expected));
}