Docs
Webhooks
Receive signed, real-time platform events at your own HTTPS endpoints, with signature verification and automatic retries.
What webhooks are
Webhooks push events to you the moment they happen, instead of you polling the REST API. Register an HTTPS endpoint and CrunchJunkie will POST a signed JSON payload to it whenever a subscribed event fires — so you can raise a Slack alert on a visibility drop, kick off a workflow when a scan finishes, or mirror events into your own database without a cron of your own.
Every delivery is signed (so you can prove it came from us), retried automatically if your endpoint is briefly down, and logged. Webhooks are available on the Pro, Agency and Scale plans, and only a workspace owner or admin can manage them. If the workspace no longer has an active Pro, Agency or Scale plan, nothing is sent (each delivery is logged as skipped), and an owner or admin can still switch endpoints off or delete them.
Adding an endpoint
Open Settings → Webhooks and add your receiver's HTTPS URL (http:// is rejected). Choose which events it should receive — all events, or a specific subset — and optionally a label.
On save we show the signing secret exactly once. It looks like whsec_… — copy it into your receiver now; we can't show it again, only rotate it for a new one. Use the "Send test" button to fire a sample event at your endpoint and confirm it accepts and verifies the delivery before you rely on it.
The event payload
We POST a JSON envelope in the Standard Webhooks shape:
{ "id": "msg_…", "type": "signal.created", "timestamp": "2026-08-28T10:00:00.000Z", "api_version": "2026-08-01", "data": { … } }
• id — a unique message id; also sent as the webhook-id header. Use it as an idempotency key so a retry is never processed twice.
• type — the event type (see below).
• data — the full event object, so you rarely need a follow-up API call.
Return any 2xx status to acknowledge. Anything else (or a timeout past 15 seconds) is treated as a failure and retried.
Verifying signatures
Three headers accompany every delivery: webhook-id, webhook-timestamp (unix seconds) and webhook-signature. The signature is HMAC-SHA256 over the string `{id}.{timestamp}.{body}`, base64-encoded, prefixed v1,. This is the Standard Webhooks scheme, so any svix-compatible library verifies it — or verify it by hand:
import crypto from "node:crypto";
function verify(secret, headers, body) {
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const signed = `${headers["webhook-id"]}.${headers["webhook-timestamp"]}.${body}`;
const expected = "v1," + crypto.createHmac("sha256", key).update(signed).digest("base64");
return headers["webhook-signature"].split(" ").some(s => crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}
Always verify against the RAW request body (before JSON parsing) and reject deliveries whose webhook-timestamp is more than five minutes old to stop replays.
Events, retries and auto-disable
Current event types:
• signal.created — a new Signal was detected (visibility drop, competitor overtake, GEO-score drop, dead source, cross-channel, a Meta ad account that stopped delivering). Only genuinely new signals fire; a continuously-true finding won't spam you. The payload carries n with nUnit (what it counts) and a confidence only where a test computed one (confidenceKind tested; null for a counted fact). title and detail are the English words stored with the signal, kept as they were; text ({ lang, title, detail }) adds the same words in the workspace's language, written from the signal's evidence. An automation's webhook action sends the same text beside title and detail.
• scan.completed — an AI-visibility scan ended, and data.status says how: done (every check saved), error (it finished with checks that failed for good), or stopped (someone stopped it — the Stop button, or deleting its client — or the Managed AI budget paused it). A scan that another one continues sends no event of its own: the scan that replaced it (a person's Run scan replacing the automatic first scan or a scan that had not started) or took over from it (a queued scan taking over from one that was driven and got nowhere) sends one when it ends, with the ids of every scan it continued in data.replaced. No event is sent for a queued scan cancelled before it started (it never ran), for a scan the stalled-scan check ends (no progress for hours, or a week old), or for a scan the schedule supersedes after it had run 6 hours without finishing.
If a delivery fails we retry with exponential backoff — after 5s, then 5m, 30m, 2h, 5h and 10h — up to eight attempts spanning more than a day; the same webhook-id is reused across every retry. After many consecutive failures an endpoint is automatically disabled (with the reason shown in Settings), so a permanently-dead URL stops generating traffic; re-enable it once it's fixed. Events still queued for an endpoint when it is disabled are closed as skipped and are not resent after you re-enable it. Rotate the signing secret any time from the same screen — the old secret stops working immediately, so update your receiver in step.
Getting started Reporting Report templates Filtering, dimensions & comparisons Sharing & delivering reports White-label & branding Metrics glossary AI visibility Your own AI API keys Perception AI Shopping Visibility GEO Audit AI crawlers & crawlability Rank Tracker AI traffic from GA4 Monitoring Signals & Automations Integrations Looker Studio Connect to Claude & ChatGPT (MCP) REST API Single sign-on (SSO) Account & billing Data, privacy & security The Crunch assistant Refer a friend FAQ