Sume webhook signature mismatch? Check the secret fingerprint header
Each Sume delivery carries x-sume-webhook-secret-fingerprint. Compare it with the dashboard before debugging code. Python verifier that refuses an empty secret.

What do I check first when a Sume webhook signature does not match?
Compare the x-sume-webhook-secret-fingerprint header with the fingerprint shown next to your secret in the dashboard at /dashboard/webhooks. If they differ, you hold the wrong secret and no code change will fix it. If they match, the secret is right and the problem is the body, the timestamp or the header handling.
Neither side ever sends the secret itself, which is the point of the fingerprint. The same value appears as signing_secret_fingerprint on the job's webhook delivery receipt, so you can also read it without catching a live request.
A short decision order
Work from the cheapest check to the most expensive one.
| Check | How | If it fails |
|---|---|---|
| Fingerprint | Header vs dashboard value | Load the right SUME_COM_WEBHOOK_SIGNING_SECRET |
| Raw body | Hash the bytes before any JSON parse | Remove the parser from this route |
| Timestamp | Within 300 s of now | Fix clock drift or the replay window |
| Rotation | Header may hold several sume-v1 entries | Accept if any entry matches |
Verifier in Python
The signed string is the timestamp, a dot, and the raw body. During a secret rotation the signature header carries one sume-v1= entry per live secret, newest first, separated by commas, so the loop accepts a match on any of them. The first line refuses an empty secret, because an empty key still produces a valid-looking digest.
import hashlib, hmac, time
def verify(body: bytes, headers: dict, secret: str, tolerance: int = 300) -> bool:
if not secret:
raise ValueError("refusing to verify with an empty signing secret")
ts = headers.get("x-sume-webhook-timestamp", "")
sig = headers.get("x-sume-webhook-signature", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > tolerance:
return False
mac = hmac.new(secret.encode(), ts.encode() + b"." + body, hashlib.sha256)
want = "sume-v1=" + mac.hexdigest()
return any(hmac.compare_digest(e.strip(), want) for e in sig.split(","))
body = b'{"event":"job.completed","job_id":"job_1"}'
ts = str(int(time.time()))
sig = "sume-v1=" + hmac.new(b"whsec_test", ts.encode() + b"." + body, hashlib.sha256).hexdigest()
h = {"x-sume-webhook-timestamp": ts, "x-sume-webhook-signature": "sume-v1=00," + sig}
print(verify(body, h, "whsec_test"), verify(body, h, "other"))Keep the fingerprint in your logs
Log the fingerprint header on every rejected delivery, not the secret and not the signature. A single line of log tells you in seconds whether a rotation reached all of your instances. Pair it with the job id so a failed delivery can be matched with a redelivery request. If you run several receivers behind one load balancer, include the instance name as well, because a half-finished rollout is an easy way for two instances to hold different secrets, and the instance name turns that guess into a fact you can read in the log.
Once the secret is fixed, you do not need to wait for a real job. POST /v1/webhooks/test-deliveries sends a signed dummy webhook.test event to your URL, and POST /v1/jobs/{job_id}/webhook/redeliver re-sends a real terminal event with a fresh signature.
Sources
Related posts
More in Developers
- SvelteKit +server.ts endpoint for an AI video webhook: request.text()
A SvelteKit POST handler reads request.text(), verifies Sume's HMAC over timestamp.body with node:crypto and returns 401 for bad or missing signatures.
- Swap the AI image model without a redeploy: JSON config hot reload
Read the Sume image model id from a JSON file that reloads when it changes, so a gpt-image-1 shutdown fix is a one-line edit with no deploy. Python, stdlib.
- Test a Sume poll loop without waiting: inject sleep, assert delays
Unit test a job poll loop in milliseconds by injecting the fetch and the sleep. Assert that next_poll_after_seconds is obeyed and the 20-minute deadline holds.
- Text-to-speech API with curl and jq: one shell script to an MP3
Call the Sume TTS Router from a shell: submit with curl, loop on the status URL with jq until terminal, then download the audio artifact to voiceover.mp3.
Written by Sume