docs
Open Framecheck

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#

EventMeaning
run.completedThe run finished; inspect its result.
run.needs_reviewChanges require review.
run.failedThe run errored.
run.passedThe run passed.
baseline.updatedAn accepted baseline was set or updated.
run.changes_requestedThere are requested changes.
run.changes_clearedChange requests were cleared.
pingTest delivery.

Headers and verification#

HeaderPurpose
x-framecheck-eventEvent name.
x-framecheck-deliveryStable event identifier; deduplicate on this.
x-framecheck-endpointReceiving endpoint identifier.
x-framecheck-attemptDelivery attempt identifier.
x-framecheck-timestampUnix seconds for this delivery attempt.
x-framecheck-signatureOne 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.

Type to search all guides.