Skip to main content

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

HeaderRequiredDescription
Content-Typeyesapplication/json
x-webflow-signatureyesHex-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

  1. Verify HMAC signature. Reject with 401 on mismatch.
  2. Parse JSON body. Reject with 400 on parse failure.
  3. Look up the Site Health sites row by webflowSiteId = payload.siteId.
  4. If not found (the user disconnected the site or never owned it), return 200 { ok: true } silently.
  5. 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 returns 200),
    • 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

CodeWhen
200Valid signature — event handled or silently ignored
400Body is not valid JSON
401Missing or mismatched x-webflow-signature
500Internal 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"
warning

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:

  1. scan-worker (concurrency 3) picks the job from the scan BullMQ queue.
  2. It fetches the site's pages and runs Lighthouse via the PageSpeed Insights API for both mobile and desktop.
  3. Results are stored in scan_results; the scan row is marked complete.
  4. 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:

EventWhen it fires
scan.completedA scan finishes successfully
scan.failedA scan errors out (quota, timeout, upstream 5xx)
regression.detectedA completed scan has score drops vs the previous
budget.violatedA scan breaches a configured performance budget
site.connected / site.disconnectedWebflow 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.

tip

Want this sooner? Thumbs-up the tracker issue or email [email protected] — roadmap priorities follow demand.