Webhooks
Today, webhooks are inbound only — Site Health receives site_publish events from Webflow so it can auto-scan a site the moment it goes live.
Outgoing webhooks (Site Health → your Zapier / Make / n8n / custom URL) are on the Phase 3 roadmap; interface is sketched at the bottom of this page.
POST /api/webhooks/webflow
Receives Webflow's site_publish event.
Base URL:
https://sitehealth.octagramlabs.com/api/webhooks/webflow
This is the URL you paste into the Webflow Dashboard when creating a site-publish webhook. Site Health registers it automatically during OAuth connect, so most users never hit this page directly.
Headers
| Header | Required | Description |
|---|---|---|
Content-Type | yes | application/json |
x-webflow-signature | yes | Hex-encoded HMAC-SHA256 over the raw request body, using your Webflow Client Secret as the key |
Signature verification
Site Health verifies every request with a constant-time HMAC check:
const expected = createHmac("sha256", WEBFLOW_CLIENT_SECRET)
.update(rawBody)
.digest("hex");
timingSafeEqual(Buffer.from(signature, "hex"), Buffer.from(expected, "hex"));
If the signature mismatches, the endpoint returns 401 Invalid signature and does nothing else.
Body
Webflow sends the standard site_publish payload. Site Health only reads siteId:
{
"_id": "evt_abc123",
"siteId": "63d7a2e9b1d4f5c6a7b8c9d0",
"site": { "_id": "63d7a2e9b1d4f5c6a7b8c9d0", "name": "Acme Inc" },
"publishedBy": { "displayName": "Jane Doe" },
"publishedOn": "2026-04-17T10:00:00.000Z",
"domains": ["acme.com", "www.acme.com"]
}
Behaviour
- Verify HMAC signature. Reject with
401on mismatch. - Parse JSON body. Reject with
400on parse failure. - Look up the Site Health
sitesrow bywebflowSiteId = payload.siteId. - If not found (the user disconnected the site or never owned it), return
200 { ok: true }silently. - Otherwise call
createAndEnqueueScan(userId, siteId, "both", "webhook"), which:- Enqueues both a mobile and desktop scan,
- Dedupes against in-flight scans — if a scan is already queued or running for this site, it throws
SCAN_ALREADY_ACTIVE(logged, but the webhook still returns200), - Adds a 0–5 s random jitter to avoid a thundering herd when multiple publishes land together.
Response
Always 200 OK on a valid-signature request, even when no action was taken (missing site, already-running scan). This prevents Webflow from retrying on benign cases.
HTTP/1.1 200 OK
Content-Type: application/json
{ "ok": true }
Status codes
| Code | When |
|---|---|
200 | Valid signature — event handled or silently ignored |
400 | Body is not valid JSON |
401 | Missing or mismatched x-webflow-signature |
500 | Internal error before signature verification (rare) |
Manual test
You usually don't call this directly — Webflow does. But you can simulate a publish locally:
BODY='{"siteId":"63d7a2e9b1d4f5c6a7b8c9d0"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$WEBFLOW_CLIENT_SECRET" | awk '{print $2}')
curl -X POST https://sitehealth.octagramlabs.com/api/webhooks/webflow \
-H "Content-Type: application/json" \
-H "x-webflow-signature: $SIG" \
-d "$BODY"
Do not expose WEBFLOW_CLIENT_SECRET outside your environment. Anyone with that secret can forge webhooks and trigger scans against your Site Health account.
What happens next
When a webhook triggers a scan, the downstream pipeline is:
scan-worker(concurrency 3) picks the job from thescanBullMQ queue.- It fetches the site's pages and runs Lighthouse via the PageSpeed Insights API for both mobile and desktop.
- Results are stored in
scan_results; the scan row is markedcomplete. - If regressions are detected vs the previous scan, a digest email may fire (based on user notification settings).
You can observe the scan in real time at /sites/{siteId}/scans/{scanId} — that page uses an SSE endpoint that polls scan status every 2 seconds.
Outgoing webhooks (planned — Phase 3)
Site Health does not currently send webhooks. Phase 3 adds an outbound webhook system for Zapier / Make / n8n / custom URL integrations.
Planned event types:
| Event | When it fires |
|---|---|
scan.completed | A scan finishes successfully |
scan.failed | A scan errors out (quota, timeout, upstream 5xx) |
regression.detected | A completed scan has score drops vs the previous |
budget.violated | A scan breaches a configured performance budget |
site.connected / site.disconnected | Webflow connection changes |
Sketched subscription API:
POST /api/external/webhooks
Authorization: Bearer wsh_…
Content-Type: application/json
{
"url": "https://hooks.zapier.com/hooks/catch/…",
"events": ["scan.completed", "regression.detected"],
"siteId": "11111111-…"
}
Planned response:
{
"id": "whk_abc…",
"url": "https://hooks.zapier.com/hooks/catch/…",
"secret": "whsec_…",
"events": ["scan.completed", "regression.detected"]
}
Every outgoing delivery will include an x-site-health-signature header computed over the raw body with whsec_…, mirroring Stripe's convention. Deliveries will retry with exponential backoff for up to 24 hours.
Want this sooner? Thumbs-up the tracker issue or email [email protected] — roadmap priorities follow demand.