Webhooks

Ten events, one JSON body, an HMAC signature to check. Enough to post to Slack, open a ticket or update your own dashboard.

Set up

Settings → API and MCP → Webhooks → Add endpoint: an https:// address and the events you want. You get a secret, shown once, for checking signatures.

  • Edit changes the address and the events. The secret stays the same.
  • Test sends a ping event right away, so you can check your receiver.
  • Log shows the last deliveries and how many tries each took.

Events

Event When
issue.resolved a recheck or audit no longer finds an issue
issue.reopened a resolved issue came back
audit.finished a full audit finished (pages, open issues, scores)
score.changed the website score moved by 2 points or more after an audit
connection.broken a Google or WordPress connection stopped working
recheck.failed a recheck ran and the issue is still there, a written fix is not on the live page yet, or the recheck itself could not run
write.applied an approved change (SEO field, llms.txt, AI crawler rules) was written through WordPress or the Shopify app
write.failed an approved change could not be written
write.not_live a recheck did not find a written value on the live page
agentic.finished an Agentic browsing re-run finished or failed

Delivery

POST https://your-endpoint
Content-Type: application/json
User-Agent: MonoRanks-Webhooks/1
X-MonoRanks-Event: recheck.failed
X-MonoRanks-Delivery: <delivery id>
X-MonoRanks-Signature: sha256=<hex>

Every body has id (the delivery id), event, at, workspaceId and siteId; most also have host and a link to the screen in the app.

{
  "id": "…",
  "event": "recheck.failed",
  "at": "2026-10-01T09:12:00.000Z",
  "workspaceId": "…",
  "siteId": "…",
  "issue": { "id": "…", "ruleId": "SEO-ONP-011", "title": "Missing meta description", "severity": "serious", "pages": 3 },
  "crawlId": "…",
  "reason": "still_present",
  "recheck": { "present": 3, "checked": 4 },
  "link": "https://app.monoranks.com/sites/…/issues/…"
}

What each event adds:

  • issue.resolved, issue.reopened: issue (id, ruleId, title, severity, pages) and crawlId.
  • audit.finished: crawlId, status, pagesCrawled, openIssues and score (site, health, aeo).
  • score.changed: from, to, delta and since.
  • connection.broken: connection (for example gsc or wordpress) and error.
  • recheck.failed: issue, reason (still_present, not_live or error) and recheck (present, checked).
  • write.applied, write.failed, write.not_live: one event per approved batch, with batchId, issueIds, source (wordpress or shopify) and up to 50 changes, each with url, field, value, status and error. For write.not_live, each change also has notLiveReason and liveValue (what the page shows).
  • agentic.finished: jobId, url, strategy, status (done or failed), passed of applicable, the failing checks and error.

Answer with any 2xx within 20 seconds. Anything else is tried again.

Checking the signature

The signature is an HMAC-SHA256 of the raw request body with your endpoint secret:

import { createHmac, timingSafeEqual } from 'node:crypto';
const expected = 'sha256=' + createHmac('sha256', SECRET).update(rawBody).digest('hex');
const ok = timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers['x-monoranks-signature']));

Use the raw body as received, not a re-serialized copy.

Common questions

What if my endpoint is down?

A failed delivery is tried again after 1, 5, 15 and 60 minutes, five tries in all. After 20 failures in a row the endpoint is switched off. Each delivery has an id in the X-MonoRanks-Delivery header so you can drop duplicates.

Can I filter by website?

The body carries the website id (siteId) and usually its host; filter on your side. Endpoints are per workspace.

I added an endpoint before the newer events existed. Do I get them?

No. An endpoint keeps the events it has ticked. Edit it to add the new ones.