docs · for developers

Webhooks

Push notifications for the things your systems care about: a finished scan, a new tracker, a published version, a completed proof export. Signed, retried, and inspectable.

Endpoints

Create endpoints under Workspace settings → Developers → Webhooks or through the API. Only https URLs on public hosts are accepted; private and internal addresses are refused at registration. The signing secret (whsec_…) is shown once at creation and can be rotated at any time; after a rotation, verify against both secrets until your deploy has caught up.

Events

Twelve event types, subscribable individually or with *:

scan.completed · scan.failed · tracker.new · issue.opened · issue.resolved · alert.raised · config.published · version.promoted · export.completed · declaration.updated · install.broken · plan.changed

Verifying a delivery

Deliveries follow the Standard Webhooks convention. Three headers arrive with each POST:

webhook-id:        <delivery id>
webhook-timestamp: <unix seconds>
webhook-signature: v1,<base64(hmac_sha256(key, id.timestamp.body))>

The HMAC key is the base64 part of your whsec_ secret, decoded to raw bytes. Recompute the signature over id + "." + timestamp + "." + body, compare in constant time, and reject anything older than five minutes:

import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(secret, headers, rawBody) {
  const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
  const msg = headers['webhook-id'] + '.' + headers['webhook-timestamp'] + '.' + rawBody;
  const want = createHmac('sha256', key).update(msg).digest();
  const got = Buffer.from(headers['webhook-signature'].split(',')[1], 'base64');
  const fresh = Math.abs(Date.now() / 1000 - Number(headers['webhook-timestamp'])) < 300;
  return fresh && want.length === got.length && timingSafeEqual(want, got);
}

Retries and redelivery

A failed delivery retries with backoff: 1 minute, then 5 minutes, 30 minutes, 2 hours, 6 hours; after that it is marked dead. Every attempt, response code and next retry is visible in the endpoint's delivery log, and any delivery can be re-sent by hand; a redelivery references the original. Delivery history is kept for 90 days.

Answering

Respond with any 2xx quickly and do the real work asynchronously; anything else counts as a failure and schedules a retry. Deliveries can arrive out of order and, rarely, more than once; use webhook-id for idempotency.