Notion's webhook setup has a step that trips almost everyone: when you create a
subscription, Notion sends a single POST to your URL containing a
verification_token — and you must read that token out of the request and
paste it back into the Webhooks tab of your integration to activate the
subscription. If your endpoint wasn't deployed, wasn't logging bodies, or your framework
swallowed the request, the token is gone: it is only sent at creation time. That's why
"Notion webhook verification" is mostly a question of catching one request.
{"verification_token": "secret_…"} to that URL — once.Point the subscription at a Hookden bin first:
/h/… URL as the webhook URL.verification_token POST appears in the bin dashboard within a second or two, body stored byte-exact.Because the bin answers 200 instantly, the delivery always succeeds — no
"endpoint unreachable" dance while your real handler is still half-written. When your
handler is ready, either recreate the subscription against your real URL (you'll get a
fresh token to catch — this time your handler can log it), or keep the bin in front and
relay to localhost while you develop.
Every subsequent event carries:
X-Notion-Signature: sha256=461e8cbcba8a75c3edd866f0e71280f5a85cbf21eff040ebd10fe266df38a735
That's sha256= + hex HMAC-SHA256 of the raw request body
— exactly GitHub's X-Hub-Signature-256 wire format. The difference is the
key: GitHub lets you choose a secret; Notion's key is the
verification_token it issued to you. Common verification failures,
in the order to check:
verification_token
(secret_…) — not your integration's API key ("Internal Integration Secret").
If you stored the wrong one, nothing will ever verify.sha256=;
compare against "sha256=" + hexdigest, or strip the prefix first — and
compare with a constant-time function.Two fast ways to check a real delivery: set the bin's signature scheme to Notion and paste the token — every capture gets a ✓/✗ badge; or paste body + header + token into the client-side signature debugger (nothing leaves your browser).
page.content_updated are batched over a short window to reduce noise — wait
a minute or two when testing, this is normal.timestamp field to reorder, and treat the payload as a change
notification, not current state — fetch the latest data from the API.Related: GitHub (same signature wire format, different key story), Slack URL verification and Zoom CRC (the other setup-handshake families), and webhooks to localhost for developing your handler behind the bin.
No signup needed. Or from your terminal: curl https://hookden.pages.dev/new
← All guides · Signature debugger · Payload examples · Docs · Hookden vs webhook.site