# Webhook reference

Receive signed Framecheck events in your own tools.

## Create an endpoint
An admin adds an HTTPS endpoint in project notification settings and selects the events to receive. Use a public HTTPS address on the standard port, without embedded credentials or a fragment. Private network addresses are not supported.

Save the signing secret securely when it is shown. Send a test delivery and inspect the payload before enabling production processing.

## Events
| Event | Meaning |
| --- | --- |
| `run.completed` | The run finished; inspect its result. |
| `run.needs_review` | Changes require review. |
| `run.failed` | The run errored. |
| `run.passed` | The run passed. |
| `baseline.updated` | An accepted baseline was set or updated. |
| `run.changes_requested` | There are requested changes. |
| `run.changes_cleared` | Change requests were cleared. |
| `ping` | Test delivery. |

## Headers and verification
| Header | Purpose |
| --- | --- |
| `x-framecheck-event` | Event name. |
| `x-framecheck-delivery` | Stable event identifier; deduplicate on this. |
| `x-framecheck-endpoint` | Receiving endpoint identifier. |
| `x-framecheck-attempt` | Delivery attempt identifier. |
| `x-framecheck-timestamp` | Unix seconds for this delivery attempt. |
| `x-framecheck-signature` | One or more `v1=` HMAC-SHA256 signatures. |

Verify against the **raw request body**, before parsing or reserialising JSON. The signed message is the timestamp, a full stop, then the raw body. Use constant-time comparison, reject stale timestamps, and deduplicate deliveries.

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

export function verifyFramecheck(rawBody, timestamp, signature, secret) {
  if (!/^\d+$/.test(timestamp ?? '') || !secret) return false;
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > 300) return false;
  const expected = createHmac('sha256', secret)
    .update(`${timestamp}.`).update(rawBody).digest();
  return String(signature ?? '').split(',').some(part => {
    const match = /^v1=([a-f0-9]{64})$/i.exec(part.trim());
    return match !== null && timingSafeEqual(expected, Buffer.from(match[1], 'hex'));
  });
}
```

This example uses a five-minute timestamp tolerance as a receiver policy. Read `rawBody` as bytes from your framework before its JSON parser. Only process the event after verification. Persist the delivery ID with your work so a retry cannot trigger the same action twice.

## Payload
The current payload has `schema: 2` and `apiVersion: '2026-10-01'`. Top-level fields include `id`, `event`, `type`, `createdAt`, `company`, `project`, `run`, and `baseline`. A test event can have no run; a baseline event can include baseline details. Handle nullable fields and tolerate additive fields.

A run includes its URL, branch, commit, pull-request number, result, review counts, snapshot counts, and accessibility summary. Payloads carry counts rather than screenshot URLs, story names, or accessibility excerpts. The result can be `passed`, `needs_review`, `failed`, or `changes_requested`.

## Rotation and redelivery
Rotating a signing secret normally leaves a 24-hour overlap with the old secret. During overlap the signature header can contain several `v1=` values; accept a valid matching signature. Update your receiver, test delivery, then retire the old secret. Expire it immediately if it was exposed.

Retries and manual redeliveries retain the same delivery ID. Use the delivery log to investigate failures and redeliver retained events after fixing your receiver.
