Webhooks
Receive events, verify the signature in eight languages, and handle retries and failures.
Webhooks tell your server about events after the original call, such as a new sanctions match for someone you monitor.
Register an endpoint#
Add an endpoint in the dashboard under Webhooks, or with POST /api/v1/webhooks and a live key. Test keys can list endpoints but cannot create, change or delete them. Each endpoint gets its own signing secret. Your URL must be HTTPS.
Events#
| Event | When |
|---|---|
verification.completed | An identity verification finished and was approved. |
aml.hit | Monitoring found a new possible match for a subject. |
aml.monitor.suspended | Monitoring was paused because your plan no longer includes it. It resumes when you upgrade. |
{
"monitorId": "mon_...",
"entityId": "seller_123",
"entityType": "person",
"name": "Test Person",
"hit": { "id": "...", "caption": "...", "score": 0.96, "datasets": [], "topics": ["sanction"] },
"classification": { "sanctionsMatch": true },
"riskLevel": "high",
"detectedAt": "2026-10-25T10:00:00.000Z",
"previousScreenAt": "2026-10-24T10:00:00.000Z",
"totalHits": 1,
"links": { "monitor": "/api/v1/aml/monitor/mon_..." },
"metadata": {}
}Request headers#
| Header | Value |
|---|---|
X-TrustVerify-Event | The event name, for example aml.hit. |
X-TrustVerify-Signature | Hex HMAC-SHA256 of the raw request body, keyed with your endpoint secret. |
X-TrustVerify-Delivery-Id | A unique ID for this delivery. Use it to ignore duplicates. |
Verify the signature#
Compute HMAC-SHA256 over the raw body bytes, before any JSON parsing, using your endpoint secret. Compare it with the header in constant time. Reject the request if they differ. Every sample below is run in our test suite against a valid and a tampered body.
// Node.js 18+. Verify a TrustVerify webhook before you trust its contents.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyTrustVerifySignature(rawBody, signatureHeader, secret) {
if (typeof signatureHeader !== "string" || !/^[0-9a-f]{64}$/i.test(signatureHeader)) return false;
const expected = createHmac("sha256", secret).update(rawBody).digest();
return timingSafeEqual(expected, Buffer.from(signatureHeader, "hex"));
}
// Express: read the body as raw bytes, verify, then parse.
// app.post("/webhooks/trustverify", express.raw({ type: "application/json" }), (req, res) => {
// const ok = verifyTrustVerifySignature(req.body, req.get("X-TrustVerify-Signature"), process.env.TRUSTVERIFY_WEBHOOK_SECRET);
// if (!ok) return res.sendStatus(401);
// const event = JSON.parse(req.body.toString("utf8"));
// res.sendStatus(200); // reply quickly, then process event in the background
// });
// Command-line check: cat body.json | TV_SIGNATURE=... TRUSTVERIFY_WEBHOOK_SECRET=... node node.mjs
if (import.meta.url === `file://${process.argv[1]}`) {
const chunks = [];
for await (const c of process.stdin) chunks.push(c);
const ok = verifyTrustVerifySignature(Buffer.concat(chunks), process.env.TV_SIGNATURE, process.env.TRUSTVERIFY_WEBHOOK_SECRET);
console.log(ok ? "valid" : "invalid");
process.exit(ok ? 0 : 1);
}No timestamp in the signature
The signature covers the body only, so it does not stop an old delivery being replayed. Record each X-TrustVerify-Delivery-Id and ignore any you have already processed.
Responding, timeouts and retries#
- Return any 2xx status within 10 seconds. Do the work afterwards, for example on a queue.
- Redirects are not followed. Register the final URL.
- If we cannot reach your endpoint (a network error or a timeout), we try up to 3 times in all: the first attempt, then retries after 4 and 8 seconds.
- A response with a non-2xx status is recorded as failed and is not retried. Only return an error when you want the delivery dropped.
- When the last attempt fails, we email the account owner.
Testing locally#
Run any sample above from the command line with a saved body to check your secret and code:
export TRUSTVERIFY_WEBHOOK_SECRET="whsec_..."
export TV_SIGNATURE="$(openssl dgst -sha256 -hmac "$TRUSTVERIFY_WEBHOOK_SECRET" < body.json | sed 's/^.* //')"
node verify.mjs < body.json # prints "valid"