Webhook security best practices: a receiver checklist

Accept HTTPS only, verify an HMAC over the raw body in constant time, reject stale timestamps, dedupe on the event id, and answer 2xx fast.

5 min readSume
All posts

Treat every webhook as untrusted until you verify it. Accept it only on an HTTPS endpoint, check its HMAC signature over the raw body in constant time, reject timestamps outside a short window to stop replays, dedupe on the event id, answer 2xx fast and do the work afterwards, and read anything that matters back from the API.

Sume's deliveries carry what each check needs: a signed timestamp, a stable event id, and one signature per live secret during a rotation. The Sume details come from its Run webhooks, Webhooks, Runs and results and Verifying webhooks docs, read on 2026-09-28.

What should a webhook receiver check?

Six checks, each answering a different attack or failure. The right-hand column is what Sume provides for it:

From Run webhooks, Webhooks and Runs and results, read 2026-09-28.
CheckStopsOn Sume
HTTPS-only endpointReading or changing deliveries in transitLocalhost, private ranges, credentials in the URL and plain HTTP are refused at create and checked again at delivery
Signature over the raw bodyForged or altered deliveriesHMAC-SHA256 over <timestamp>.<raw_body>, sent as sume-v1=<hex_signature>
Timestamp windowReplays of an old, genuine deliveryx-sume-webhook-timestamp; five minutes is a reasonable default
DeduplicationProcessing one event twicerequest_id on run webhooks, job_id on job webhooks
Fast 2xxTimeouts that trigger retries10 seconds per attempt, up to 10 attempts
Confirm with the APIActing on a payload aloneA run webhook's payload is byte-identical to the run you can read back

How do I verify a webhook signature correctly?

Four rules make the check hold. In current code, verifyWebhook in Sume's TypeScript SDK handles the last three; the first is up to you. Signed webhooks for Sume video runs shows the SDK handler, and the Python and Go posts write the same checks by hand.

  • Verify the raw bytes, before any JSON parse. A parsed-and-reserialized object does not verify, because key order and whitespace are part of what was signed.
  • Parse the signature header; never compare it whole. During a secret rotation, Sume sends one sume-v1= entry per live secret, comma-separated, newest first, and a delivery is valid when any entry matches. Skip entries without the sume-v1= prefix.
  • Compare in constant time, and compare every entry even after a match, so timing can't reveal which secret matched.
  • Refuse an empty secret. An empty key is one anyone can use, so an unset environment variable must fail the check; in current code the SDK returns false when the secret is empty.

How do I stop webhook replay attacks?

A replay attack resends a genuine, validly signed request that someone captured. The signature can't catch it, because the bytes are real. Two checks do, and one caveat applies:

  • Reject stale timestamps. Sume signs <timestamp>.<raw_body>, so the timestamp can't be changed without breaking the signature. Reject a delivery whose x-sume-webhook-timestamp is outside your window; the SDK's toleranceSeconds defaults to 300.
  • Dedupe on the event id. A replay inside the window carries the same request_id (runs) or job_id (jobs) as the original, and so does each of Sume's own retries. Record the ids you've processed and ignore repeats.
  • Expect legitimate repeats as well. Sume's Redeliver re-sends a real event with a fresh timestamp and signature, so it passes the window check; the id is what marks it as a repeat.

Are webhooks secure?

As secure as the receiver makes them. A webhook URL is public, so anyone can send it a request; the signature is what proves a delivery came from someone holding the secret. That makes the secret the thing to protect.

Sume derives the signing secret for your workspace, so nobody else's secret verifies a delivery signed for you. Store it the way you store the API key, as in Where to store API keys, and rotate it if it may have leaked; Signed webhooks for Sume video runs covers the rotation window. When a signature won't verify, compare the x-sume-webhook-secret-fingerprint header with the fingerprint in the dashboard rather than pasting the secret anywhere.

What should the endpoint do after it verifies?

  • Record the event durably, answer 2xx, then process. A slow endpoint burns the 10-second attempt budget and gets retried.
  • Answer an unknown event type with 204. Sume's docs say this stops a newly added event type from becoming a 500 and a retry storm.
  • Register the final URL. Redirects are not followed, and a 3xx counts as a failed attempt; Webhook URL rejected as invalid? lists Sume's other URL rules.
  • Confirm before you act on anything costly. A run webhook's payload is byte-identical to data from GET /v1/format-runs/{run_id} for a Format run, so you can read the run back from the API first. Debug Sume webhook delivery covers deliveries that fail.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume