Skip to content
Guides

Webhooks

Register an https endpoint in the dashboard. Each endpoint carries its own signing secret and its own disclosure class.

what arriveshttp
POST /your/endpoint HTTP/1.1
content-type: application/json
webhook-id: msg_2fk9xq7t4b1a
webhook-timestamp: 1756512000
webhook-signature: v1,K8mN2pQ...

{"type":"attestation.verdict","policy_id":"...","verdict":{...}}

Verify the signature

The signed string is {webhook-id}.{webhook-timestamp}.{raw body}, HMAC-SHA256 under the endpoint secret, base64, prefixed v1,. Compare in constant time and reject anything older than five minutes.

import crypto from "node:crypto";

export function verify(req: { headers: Record<string, string>; rawBody: string }, secret: string) {
  const id = req.headers["webhook-id"];
  const ts = req.headers["webhook-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const mac = crypto.createHmac("sha256", key).update(`${id}.${ts}.${req.rawBody}`).digest("base64");
  const expected = Buffer.from(`v1,${mac}`);

  return req.headers["webhook-signature"].split(" ").some((candidate) => {
    const got = Buffer.from(candidate);
    return got.length === expected.length && crypto.timingSafeEqual(got, expected);
  });
}
Validate before you act

An endpoint that skips these headers accepts forged events from anyone who learns its URL.

Events

  • attestation.verdict — a device was appraised. Can be narrowed to specific policies.
  • device.enrolled — a device completed enrolment.
  • usage.alert — active devices crossed a percentage you set of a budget you set (blank = the plan's included devices). Fires once per billing period.

Disclosure class

Per endpoint, independent of what verify requested: verdict (booleans and enums, no identifier), pseudonymous (adds the per-tenant device id; default), derived (lifecycle facts), full.

Delivery

  • At-least-once, unordered. Deduplicate on webhook-id, constant across retries.
  • 2xx is success. 3xx is a failure; redirects are not followed. 410 Gone disables the endpoint.
  • Retries run for about three days with increasing backoff. Keep the deduplication window at least that long.
  • Twenty consecutive failures disable the endpoint; re-enable it in the dashboard.
  • Rotating a secret keeps signing with the previous one for 24 hours; both ride in the same header.
  • The URL must be https, carry no credentials, and resolve to a publicly routable address on every A and AAAA record. Pro and above.