Sume webhook signature header: why a sume-v2 entry is skipped
A Sume webhook verifier should compare only sume-v1= entries from the comma-separated header and skip other prefixes. Here is a Python check with a test.

A Sume webhook verifier should split the x-sume-webhook-signature header on commas, keep only entries that start with sume-v1=, and accept the delivery if any of them matches. Entries with another prefix, such as a future sume-v2=, are skipped, not failed. The SDK's verifyWebhook is written this way so an old receiver does not break when a new scheme appears.
Why the header can hold several entries
During a signing-secret rotation, Sume signs once per live secret, newest first, comma-separated: sume-v1=<new>,sume-v1=<previous>. The version prefix is on the wire so that the scheme can change later. The SDK source says a future sume-v2= next to sume-v1= must not make an old receiver fail.
| Header | Action |
|---|---|
One sume-v1= entry | Compare it |
Two sume-v1= entries (rotation) | Accept if either matches |
sume-v2= plus a sume-v1= entry | Compare only the v1 entry |
Only sume-v2= | Reject: nothing you can check |
| Empty secret | Reject before computing anything |
A stdlib Python check
The signed string is <timestamp>.<raw_body>, HMAC SHA-256, hex. Compare every candidate so timing does not show which one matched.
import hashlib, hmac, time
def verify(raw: bytes, ts: str, header: str, secret: str, tol: int = 300) -> bool:
if not secret or not ts.isdigit() or abs(time.time() - int(ts)) > tol:
return False
mac = hmac.new(secret.encode(), ts.encode() + b"." + raw, hashlib.sha256)
want = "sume-v1=" + mac.hexdigest()
ok = False
for entry in (e.strip() for e in header.split(",")):
if entry.startswith("sume-v1=") and hmac.compare_digest(entry, want):
ok = True # sume-v2=... and other prefixes are skipped
return ok
if __name__ == "__main__":
body, ts, secret = b'{"event":"job.completed"}', str(int(time.time())), "s3cret"
sig = "sume-v1=" + hmac.new(secret.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()
print(verify(body, ts, "sume-v2=abc," + sig, secret)) # True
print(verify(body, ts, "sume-v2=abc", secret)) # False
print(verify(body, ts, sig, "")) # FalseRaw bytes only
Compute over the exact request bytes. A parsed and re-serialized body does not verify, because key order and whitespace were part of what was signed.
Sources
Related posts
More in Developers
- 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.
- Sume webhook secret is per workspace: read it with account:read
Sume derives the webhook signing secret per workspace, not as a shared platform value. Read it on the dashboard or with GET /v1/webhooks/signing-secret.
- 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.
Written by Sume