A known-answer test for the Sume webhook signature: one body, one hex

A fixed secret, timestamp, and body give a fixed sume-v1 value. Use it to unit-test your verifier: valid, stale, empty secret, and rotation cases in Python.

4 min readSume
All posts

For secret test-secret, timestamp 1780000000, and the body shown below, the Sume signature is sume-v1=0e086a3652355040de7d99ce75724a18548b2f7d1a864cec025e32173844b883. It is HMAC-SHA256 over <timestamp>.<raw_body>, as documented. A fixed vector like this catches the usual bugs: wrong separator, re-serialized JSON, and a skipped replay check.

The vector

I computed this with Python's hmac and the scheme in the Webhooks docs. It is a test fixture and has nothing to do with any real secret.

Known-answer vector for the sume-v1 scheme (computed from the documented scheme, 2026-10-09)
InputValue
Secrettest-secret
x-sume-webhook-timestamp1780000000
Raw body{"event":"job.completed","request_id":"job_demo","job_id":"job_demo","status":"OK"}
Signed string1780000000.<raw body>
x-sume-webhook-signaturesume-v1=0e086a3652355040de7d99ce75724a18548b2f7d1a864cec025e32173844b883

A verifier and four checks

The function takes now as a parameter, so the test does not depend on the clock. It refuses an empty secret, rejects a timestamp outside 300 seconds, and accepts any entry in a comma-separated header. Expected output: True, False, False, True.

import hashlib
import hmac

def verify(secret: str, timestamp: str, header: str, body: str, now: int) -> bool:
    if not secret:
        return False  # refuse an empty secret
    try:
        ts = int(timestamp)
    except ValueError:
        return False
    if abs(now - ts) > 300:
        return False
    mac = hmac.new(secret.encode(), f"{ts}.{body}".encode(), hashlib.sha256)
    want = "sume-v1=" + mac.hexdigest()
    return any(
        hmac.compare_digest(part.strip(), want) for part in header.split(",")
    )

BODY = '{"event":"job.completed","request_id":"job_demo","job_id":"job_demo","status":"OK"}'
SIG = "sume-v1=0e086a3652355040de7d99ce75724a18548b2f7d1a864cec025e32173844b883"
print(verify("test-secret", "1780000000", SIG, BODY, 1780000100))      # True
print(verify("test-secret", "1780000000", SIG, BODY, 1780001000))      # False: stale
print(verify("", "1780000000", SIG, BODY, 1780000100))                 # False: empty
print(verify("test-secret", "1780000000", "sume-v1=00,"+SIG, BODY, 1780000100))  # True

What each case proves

The first case proves the separator and encoding are right. The second proves the replay window works: the delivery is 1,000 seconds old. The third proves that an empty secret fails closed. A verifier that hashes with an empty key will accept a signature from anyone who guesses that. The fourth proves rotation support: during the 24-hour window the header carries the new and previous signatures, and either match is enough.

Do not set the tolerance to zero in production. In the SDK toleranceSeconds: 0 skips the timestamp check, which removes replay protection. If you use the SDK's verifyWebhook instead of your own code, run the same vector through it.

Using the vector in tests

Keep the vector in your unit tests next to a tampered body and a stale timestamp case. The tampered case must return false, and a timestamp outside the 300 second tolerance must return false as well. A verifier that passes the known answer but never fails a bad one is not tested.

Pass the timestamp into the function instead of reading the clock inside it, so the test is deterministic. And never set toleranceSeconds to 0 in production, since that skips the replay check.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume