Webflow signs webhook deliveries with hex HMAC-SHA256 in x-webflow-signature
— but whether you get that header at all, and which secret verifies it, both depend on
how the webhook was created. That one fact explains most “signature missing”
and “signature never matches” reports. Here's the debugging order.
If x-webflow-signature and x-webflow-timestamp are simply
absent, nothing is broken and nobody is attacking you. Webflow's docs say it plainly:
webhooks created through the site dashboard “will not include the request headers
needed to validate request signatures”. Dashboard webhooks send no signature
headers at all.
Fix: recreate the webhook through the API — either from an OAuth app or with a site token — and you'll get signed deliveries. If your handler requires the header (most SDK-based verifiers throw on a missing header), a dashboard-created webhook will fail 100% of deliveries, which also marches you toward deactivation (see the retry section below).
Webflow has two different signing keys depending on creation method:
Mixing these up — verifying a site-token webhook with your app's client secret, or vice versa — produces a signature mismatch on every single delivery. If you rotate the OAuth client secret, every OAuth-app webhook's signatures change with it.
The signed string is the timestamp and raw body joined with a colon:
expected = hex( HMAC_SHA256( key = signing_key, message = timestamp + ":" + raw_body ) )
…and x-webflow-timestamp is unix milliseconds, not
seconds. Webflow's own replay-window sample computes
currentTime - timestamp > 300000 (5 minutes in ms). Code that treats the
header as seconds either rejects every delivery as “too old” or accepts week-old
replays — the same family of bug as WorkOS's ms timestamps. The colon separator is the
other classic miss: sign ts + ":" + body, not ts + body.
Webflow's manual-validation docs show
timestamp + ":" + JSON.stringify(request_body) — stringifying the
parsed body. That only matches while your JSON parser round-trips
byte-identically (key order, unicode escapes, number formatting). Form-submission
payloads are especially risky: field names arrive as display labels with spaces
("First Name") under payload.data, and any middleware that
touches the JSON re-serializes it. HMAC the raw request bytes; read them
before any body-parser runs.
All of these count as failed deliveries: any non-200 status,
a redirect (Webflow doesn't follow them — a 301 on your webhook path
fails every delivery), SSL certificate problems, and slow responses. A failed delivery is
retried 3 times at 10-minute intervals, then it's gone — there's no
replay. Repeated failure gets the webhook deactivated, with an email
notification, and reactivation goes through Webflow support — not a dashboard
toggle. Also worth knowing: max 75 webhooks per trigger type, and
triggerType in the payload tells you which of the event types fired.
Webflow's own webhook tutorial points you at webhook.site to inspect deliveries. A Hookden bin does the same job free, and goes further: select the Webflow scheme in bin settings, paste your signing key, and every delivery gets a live ✓/✗ badge — computed over the exact raw bytes, ms timestamp and colon included.
curl -X POST "https://api.webflow.com/v2/sites/{site_id}/webhooks" \
-H "Authorization: Bearer {token}" -H "Content-Type: application/json" \
-d '{"triggerType":"form_submission","url":"https://hookden.pages.dev/h/YOUR-BIN"}'x-webflow-signature in the capture = you have a
dashboard/unsigned webhook; recreate it via the API. (See a
realistic sample payload with a verifying
signature.)Related: retry-schedule comparison across 26 providers, webhooks to localhost for developing the handler locally, and the Webflow payload example whose signature actually verifies.
No signup needed. Or from your terminal: curl https://hookden.pages.dev/new
← All guides · Signature debugger · Payload examples · Docs · Hookden vs webhook.site