Notion X-Notion-Signature vs Sume sume-v1: two verifiers
Notion signs the body with your verification token. Sume signs a timestamp plus the body. Here is how to keep the two checks apart in one receiver.

If one endpoint receives both Notion events and Sume run events, give each its own verifier. Notion signs the request body with HMAC-SHA256 keyed by the verification_token and sends it in X-Notion-Signature. Sume signs <timestamp>.<raw_body> with HMAC-SHA256 and sends x-sume-webhook-signature: sume-v1=<hex>. The two use different secrets, different inputs and different headers.
The Notion facts are from Webhooks, read 2026-10-10. The Sume facts are from Verifying webhooks and Run webhooks.
How do they differ?
Notion starts with a handshake: when you create a subscription, it sends a verification_token to your URL, and you use it to confirm and later to compute the HMAC. The body alone is signed. Sume signs the timestamp from x-sume-webhook-timestamp together with the raw body, which gives you a replay window; the SDK default is 300 seconds. Both require the public HTTPS URL.
Notion's page also notes that webhook URLs must be public HTTPS, like Sume's. If you test locally, use a tunnel for both. For Sume, remember that private and localhost hosts and explicit ports are refused, so a tunnel URL on the standard port is the right shape.
| Point | Notion | Sume |
|---|---|---|
| Header | X-Notion-Signature | x-sume-webhook-signature, value sume-v1=<hex> |
| Signed input | The request body | <timestamp>.<raw_body> |
| Key | The verification_token from the handshake | The signing secret from the dashboard or GET /v1/webhooks/signing-secret |
| Replay guard | Not part of the signature | Timestamp header, 300 second default window |
| Rotation | Not covered here | Header carries sume-v1=new,sume-v1=old for 24 hours |
How should the receiver route them?
Branch on the header before touching the body. If x-sume-webhook-signature is present, use the Sume verifier; if X-Notion-Signature is present, use Notion's. A request with neither is rejected. Never try one secret against the other's header, and never share a variable between them.
Read the raw bytes once. Parsing the JSON and re-serializing it changes the bytes and breaks both signatures.
Test each verifier with a known-bad case. Flip one byte of the body, change the timestamp by 10 minutes, and send an empty secret. All three should return false. The empty-secret case matters because a missing environment variable otherwise turns into an HMAC over an empty key, which anyone can compute.
import crypto from "node:crypto";
export function verifySume(raw, headers, secret) {
if (!secret) return false;
const ts = headers["x-sume-webhook-timestamp"];
const sigs = String(headers["x-sume-webhook-signature"] || "").split(",");
if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const want = crypto.createHmac("sha256", secret).update(`${ts}.${raw}`).digest("hex");
return sigs.some(s => {
const got = s.trim().replace(/^sume-v1=/, "");
return got.length === want.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want));
});
}What about timing differences?
Notion events are not all immediate. The page says page.content_updated is aggregated, with up to about a minute of delay, while comment.created arrives within seconds. Do not start a paid Sume run on every content update; wait for the aggregated event, then check whether the page actually changed in a way that needs a new render.
On the Sume side, a run webhook is one event per run, with request_id equal to the run id. Dedupe on it. For Notion, store the event id the payload carries.
Do not start a paid run from a bare Notion event. Verify it, check which property changed, and send the Sume request with an Idempotency-Key made from the page id and the page's last edited time, so the aggregated events cannot make two renders.
What about secret rotation?
When you rotate Sume's signing secret, deliveries carry both signatures for 24 hours, sume-v1=new,sume-v1=old, which is why the code above accepts any matching entry. Update your stored secret within that window and the cut-over is quiet. Notion's token is separate; keep the two in different environment variables and never log either.
Where you can, prefer the SDK helper verifyWebhook from @sume-com/sdk. It is async, returns false instead of throwing, and checks the replay window before computing the HMAC.
Finally, log which verifier accepted each request, but never log the secrets or the full signature header. A short label is enough to debug a mixed endpoint.
Sources
Related posts
More in Integrations
- Performance Max rejects MP3 and WAV: turn a music track into a video
Google Ads lists audio files as not accepted on YouTube for Performance Max. Render a Sume music track over a still with Timeline 1.0 into a 10-second-plus MP4.
- Podia lesson video: 30 fps or below, H.264, 8 Mbps at 1080p
Podia wants MP4, H.264, AAC, 30 fps or below, under 4 hours. Set the Sume timeline to 30 fps at 1920x1080, then check bitrate yourself, since Sume has no field.
- Render deploy hook returns 202: start a Sume run after a deploy
Render deploy hooks return 200 when a deploy starts and 202 when queued. Call one, then start a Sume Format run for the release only once it ships.
- SendGrid ECDSA event webhook vs a Sume HMAC verifier: two checks
SendGrid signs event webhooks with ECDSA, while Sume uses HMAC SHA256. Here is why one verifier will not cover both and how to start a Sume run from an event.
Written by Sume