Developers · 05

Webhooks

HTTPS POST to your URL when a matching change is recorded. HMAC-signed; verify before you act.

Raw markdown

Register

Admin → Integrations → Webhooks. Public HTTPS URL. Subscribe to the events you need (Events).

Copy whsec_… once.

Each delivery is a POST:

HeaderValue
X-Prototype-Evente.g. spec.published
X-Prototype-Deliveryuuid, safe to use as an idempotency key
X-Prototype-Timestampunix seconds
X-Prototype-Signaturesha256=<hex> of {timestamp}.{raw_body}

Respond 2xx within 10 seconds. Anything else is retried on the daily pass.

URL must be HTTPS (HTTP localhost is allowed outside production). No credentials in the URL.

Verify

Use the raw body (do not JSON.parse then stringify). Compare in constant time:

javascript
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret, timestamp, rawBody, signatureHeader) {
  const expected = "sha256=" + createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader ?? "");
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}

Reject if the signature fails, or if the timestamp is more than five minutes off. Skip duplicate X-Prototype-Delivery ids.

Payload

json
{
  "id": 1842,
  "type": "spec.published",
  "action": "published",
  "target_kind": "specification",
  "target_id": "0f3c…",
  "actor_id": "user_…",
  "occurred_at": "2026-08-22T05:12:00.000Z",
  "diff": { "before": { "value": 3.8 }, "after": { "value": 4.2 } }
}

type is the event you subscribed to. target_kind and diff differ by event; Events shows an example for each. There is no project slug on the payload. List /projects, then the nested collections, if you need to fan out.

Ping: POST /api/v1/webhooks/ping with {"webhook_id":"<uuid>"} (scope webhooks:manage) sends a synthetic change.created.