Webhook events
Subscribe to case lifecycle events with HMAC-SHA256 signed payloads. Configure endpoints from the Integrations page in the dashboard.
Event catalog
Three event types fire today. Adding a fourth is non-breaking — your endpoint receives only the events you subscribed to, so new types are opt-in.
| Event | When it fires |
|---|---|
case.completed | Analysis pipeline finished for a case (any verdict). |
case.failed | Pipeline crashed before producing a verdict — retry or reupload. |
case.high_fraud_risk | Verdict is LIKELY_MANIPULATED or LIKELY_AI_GENERATED with high confidence. |
Payload envelope
Every event ships in a stable wrapper. The event-specific fields live under data; the wrapper itself never changes.
{
"id": "evt_8a1c2d3e-4f5g-6789-abcd-ef0123456789",
"type": "case.completed",
"created": "2026-04-25T18:42:11.482Z",
"data": {
"caseId": "ce_01J9XJ3K4Q9R5S6T7U8V9W0X1Y",
"verdict": "SUSPICIOUS",
"confidence": 62,
"criticalFindings": 0,
"imageCount": 3,
"completedAt": "2026-04-25T18:42:11.391Z"
}
}Signature verification
Every delivery carries an X-Claimscan-Signature header so you can prove the request really came from Claimscan and was not tampered with in transit. Reject any request that does not verify.
The header packs a unix timestamp and a hex-encoded HMAC digest, comma-separated:
X-Claimscan-Signature: t=1714069331,v1=8e2c9f4b1d7a6e3c0b8f5a2d9c1e6b4f3a7d8c2e5f9a1b4c7d0e3f6a9b2c5d8eVerify each delivery in four steps:
- Parse the header into t (timestamp) and v1 (signature) parts.
- Reject if abs(now - t) > 300 seconds — protects against replay.
- Compute HMAC-SHA256 over `${t}.${rawBody}` using your endpoint secret. Use the raw request body, not a re-serialised JSON string.
- Compare to v1 with a constant-time comparison (timingSafeEqual in Node).
Reference implementation (Node)
import { createHmac, timingSafeEqual } from "node:crypto";
/**
* Verify a Claimscan webhook signature. Reject if:
* - timestamp is older than 5 minutes (replay protection)
* - HMAC-SHA256 over `${timestamp}.${rawBody}` does not match v1
*/
export function verifyClaimscanSignature(opts: {
header: string; // X-Claimscan-Signature
rawBody: string; // request body BEFORE JSON.parse
secret: string; // whsec_<base64url>
}): boolean {
const parts = Object.fromEntries(
opts.header.split(",").map((p) => p.split("="))
);
const t = Number.parseInt(parts.t ?? "", 10);
const v1 = parts.v1;
if (!Number.isFinite(t) || !v1) return false;
const skew = Math.abs(Math.floor(Date.now() / 1000) - t);
if (skew > 5 * 60) return false;
const expected = createHmac("sha256", opts.secret)
.update(`${t}.${opts.rawBody}`)
.digest();
const actual = Buffer.from(v1, "hex");
if (actual.length !== expected.length) return false;
return timingSafeEqual(actual, expected);
}Retry policy
Failed deliveries (network error or HTTP 4xx/5xx other than 410 Gone) retry up to four times with exponential backoff:
| Attempt | Delay after previous failure |
|---|---|
| 1 | Immediate |
| 2 | ≈ 30 seconds |
| 3 | ≈ 5 minutes |
| 4 | ≈ 30 minutes |
After the fourth failure the delivery is marked failed in your dashboard and we stop. Inspect the delivery log to retry manually after fixing your endpoint.
Security notes
- Webhook URLs must be public HTTPS. We block private IP ranges, link-local and metadata addresses (SSRF guard) at registration time.
- HTTP URLs are rejected. Self-signed certificates are not supported in production.
- Treat the endpoint secret like an API key — store it server-side, never in client code, and rotate it from the dashboard if you suspect exposure.
- Always treat deliveries as at-least-once. Use the envelope id as a deduplication key on your side.
Manage endpoints
Configure, test and rotate webhook endpoints under Integrations → Webhooks in the dashboard.