Webhooks
Helix delivers risk decisions to your systems by HTTP POST. Every delivery is signed with HMAC-SHA256 so you can prove the request came from us.
Signature format
Header: x-helix-signature: t=<unix-seconds>,v1=<hex>
Where v1 = HMAC_SHA256(secret, "<t>.<raw-body>").
Verifying in your handler (Node)
import { createHmac, timingSafeEqual } from "crypto";
const TOLERANCE = 300; // seconds
function verify(rawBody: string, header: string | null, secret: string) {
if (!header) return false;
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=")),
);
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > TOLERANCE) return false;
const expected = createHmac("sha256", secret)
.update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1 ?? "", "hex");
return a.length === b.length && timingSafeEqual(a, b);
}
Rules
- Use the raw request body string. JSON-parse only after verifying.
- Reject anything older than 5 minutes (replay protection).
- Use
timingSafeEqual— never===. - The secret is shown ONCE when the webhook is created. If you lose it, rotate the webhook to mint a new one.
- We retry failed deliveries (2xx = success) up to 5 times with
exponential backoff. Idempotency: dedupe on
event.id.
Testing
The control plane has a Send test event button on each webhook. It
POSTs a signed payload of {"type":"helix.test", ...} and records the
result in the audit log.
Delivery history
Workspace owners can review every signed delivery (status code, payload hash, attempt number) under Webhooks → Delivery log. The payload hash is a SHA-256 of the body — useful for proving to a bank's audit team that a specific event was sent without storing the body itself.