Skip to content

Webhooks

Under Settings, API & webhooks an admin adds an HTTPS endpoint and picks the events it should receive. The signing secret is shown once, when the endpoint is created. Up to ten endpoints per workspace.

Event When data
run.completed a full or change run completed readership (the run’s headline audit figures) and change (counts of new, resolved, regressed and still-open findings; pages compared, changed, gone, newly blocked or served; AI access before and after), change is null on a site’s first run
run.failed a run failed or paused on its budget status, error
finding.new_severe a run found new critical or high audit findings findings: up to 50 with key, title, severity, category, scope
fix.verified a run checked recorded fixes confirmed and refuted: key, title, severity and the effect sentence
access.changed the daily access watch found a change checkId, changes (each with its kind: refused, served, gone, robots, llms, and the identity and URLs it concerns) and sentences, the same lines the workspace shows. run is null: a check is not a run
citation.changed a run’s own engine check found the site dropped out of, or entered, an answer it was in before against (the previous run with an engine check), dropped and gained: each with query, engine, label, instead (the domains cited in its place) and url (the page of the site that was cited)
traffic.changed the weekly traffic check found the sessions AI assistants sent in the last complete week more than 30% below the average of the four weeks before (Google Analytics); or the daily access watch found, in the crawlers’ own traffic, an AI crawler newly refused or newly served at the edge, or a crawler gone quiet kind tells the two shapes apart, and both carry source (analytics or crawler_traffic), sentence (one line) and sentences (the lines, one for the weekly check), so a receiver can read either the same way and switch on kind for the rest. kind: "ai_sessions_drop": week (its Monday), sessions, mean (of the four weeks before) and dropPct. kind: "crawler_observed": checkId and changes (each with kind observed, with crawler, label, what, hits, before and after as refused shares, or quiet, with crawler, label, weeklyBefore, lastSeen). run is null
prompt.changed an engine’s answers to a question the workspace tracks word for word started or stopped naming or citing the site (two answers in a row) question (id, text, url), prompt (null: the tracked-prompt list is retired) and engines: each with engine, label, changes and sentences. run is null
engine.drift Vercatus Intelligence confirmed a drift that touches the site noticeId, headline, drift (the engine, the page feature, the direction, the window, the size of the change and the medians of the pages that entered and were replaced) and exposure (losing of of observed pages, siteMedian). run is null
competitor.changed a competitor’s page score from the audit passed the site’s, overall or on one engine noticeId, competitor, why (named or cocited), scope, day, before and now. run is null
crawl.slowing the daily check found a search crawler refetching the site’s money pages less often, or less successfully, than its usual range, before any citation loss shows crawler, label, template, week, sentence, rate and success (the checks with their figures), days, panel (whether the run’s engine check already shows a loss), noticeId
disclosure.missing a disclosure check (after a run, or a live check) found a required disclosure the workspace defined gone from a page, or from one of its views (JavaScript off, rendered, a crawler’s), where the previous check found it noticeId, checkId, trigger (schedule, manual or live), workflowId (the run whose pages were read; null for a live check), lost (each with ruleId, rule, url and view) and sentences. run is null
agent_task.failed an agent task trial that passed under an identity in its previous batch failed under it in this one noticeId, taskId, task, kind, batchId and failed (each with identity, blocker, detail, finalUrl and steps). run is null
journey.checkpoint a site reached a checkpoint against its first audit’s starting point (day 14, 30, 60, 90, 180, 270, then each year), one delivery per checkpoint; kept for integrations built on it day, label, claim, headline, indexBefore, indexNow, fixed, total
answer.stale a scan of the site’s latest answers found an engine quoting a fact about the site (a price, a plan, a figure, a date) that the site’s page no longer states, one delivery per new stale answer noticeId, staleId, engine, question, kind, answerValue, siteValue, pageUrl, siteSince (the first snapshot of the page stating the current value), cause (not_refetched, refused, refetched, never_fetched or no_crawl_data), crawl (per crawler of the engine: lastServedAt, served, refused, and which version of the page it saw), severity and sentence. run is null
verdict.read a play’s verdict landed: a read of Won or Lost ground (once for each state; a read before day 45 is provisional), or the final day-45 read, whatever it says; one delivery per read, with the same delivery id if it is ever sent again. A Too early read, and a flat or unclear read before day 45, are not sent noticeId, verdictId, day (14, 30 or 45), state (won, lost_ground, no_effect, inconclusive) and label (Won, Lost ground, No clear change, Inconclusive), grade (A to D), final and provisional, play (id, title; null for plays measured together) and plays, topics (id, name), engines, effect (pp, lo, hi, in percentage points of Answer Share against the topics left alone, with unit), mde, multiplicity (q and compared when several plays were read the same day, or null), sentence and url. The wording says answers moved, with the range and the grade: never that the play proved or caused it. run is null
visibility.changed once a day at most, when the site’s Answer Share, a topic’s or a competitor’s moved clearly over the last 7 days against the 7 before (its range excludes zero on enough answers); a day with no clear move sends nothing noticeId, days (7), window (from, to, days), moves (each with kind: you, topic or competitor, key, name, now (the share in percent), delta, lo, hi in percentage points). run is null

One POST per event per endpoint, Content-Type: application/json, from user agent Vercatus-Webhooks/1.

{
"event": "fix.verified",
"deliveryId": "8f2c1d0a9b3e4f61",
"at": "2026-09-16T17:09:44.901Z",
"org": { "id": "…", "name": "Acme" },
"site": { "id": "…", "host": "www.acme.com" },
"run": { "id": "…", "kind": "light", "status": "completed", "url": "https://app.vercatus.com/runs/…", "startedAt": "…", "finishedAt": "…" },
"data": { "confirmed": [ { "key": "b3a71480fbf3d00a", "title": "No /llms.txt", "severity": "low", "effect": "…" } ], "refuted": [] }
}

Headers:

Header Value
x-vercatus-event the event name
x-vercatus-delivery the delivery id; the same id is kept on retries and when a delivery is sent again from the log
vercatus-signature t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">, keyed with the endpoint’s secret. While a secret is being rotated there are two v1 values, one per secret
x-vercatus-signature deprecated: sha256= and the hex HMAC-SHA256 of the raw body alone. Still sent for one release; move to vercatus-signature, which also protects against replays

Any 2xx counts as delivered. A 5xx, 408, 425, 429, a timeout or a network error is retried: after half a minute, a minute, five minutes, then with growing gaps up to six hours, for about a day in all. Any other 4xx is taken as the endpoint’s answer and not retried. Redirects are not followed. Deliveries go out from a queue, so a restart on our side delays a delivery but does not lose it.

Endpoints must be HTTPS addresses on the public internet; addresses that resolve to a private, loopback or link-local network are refused.

Compute the HMAC over the timestamp, a dot and the exact bytes received, before parsing; compare with a constant-time comparison against each v1 value; and refuse a timestamp more than five minutes old, so a captured delivery cannot be replayed:

import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, header, secret, toleranceSeconds = 300) {
const parts = header.split(",").map((p) => p.trim().split("="));
const t = Number(parts.find(([k]) => k === "t")?.[1]);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = Buffer.from(createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"));
return parts.some(([k, v]) => k === "v1" && v.length === expected.length && timingSafeEqual(Buffer.from(v), expected));
}

Use deliveryId to ignore a delivery you already processed. Each delivery also carries sequence in the body and in an x-vercatus-sequence header: a number that only grows (microseconds since the epoch, taken when the event was queued). Retries can bring two deliveries of one subject out of order; keep the one with the higher sequence. Send a test on the settings page posts a run.completed with data.test: true, signed like any other delivery; the answer shows the status your endpoint returned, never its body.

An endpoint that fails 25 attempts in a row (any attempt that is not a 2xx, counting retries) is paused: it stops receiving, the endpoint list shows why and when, and the deliveries waiting for it are kept, not dropped. Resuming it (Settings, API & webhooks, or PATCH /api/org/webhooks/:id with { "active": true }) sends what was held at once. One delivered attempt resets the count.

Settings, API & webhooks lists the deliveries of the last 30 days with their result, the status your endpoint answered and the next attempt, and Send again queues one again with the same payload and id.

Rotate signing secret makes a new secret and shows it once. For 24 hours every delivery carries two v1 signatures, one per secret, so the endpoint can switch to the new secret without missing a delivery.

POST /api/org/webhooks with { url, events } returns the endpoint including its secret, once. PATCH /api/org/webhooks/:id with { active?, events?, url? } (unknown event names are refused), DELETE /api/org/webhooks/:id, POST /api/org/webhooks/:id/test, POST /api/org/webhooks/:id/rotate with { graceHours? } (0 to 168, default 24) returning the new secret once, GET /api/org/webhooks/deliveries (?hook=, ?status=pending|delivered|failed, ?limit=) and POST /api/org/webhooks/deliveries/:id/redeliver. Admin role required.