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.

5 min readSume
All posts

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 cases for a v1 verifier (read 2026-10-06)
HeaderAction
One sume-v1= entryCompare it
Two sume-v1= entries (rotation)Accept if either matches
sume-v2= plus a sume-v1= entryCompare only the v1 entry
Only sume-v2=Reject: nothing you can check
Empty secretReject 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, ""))                       # False

Raw 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

All Developers posts

Written by Sume