Test a Sume webhook receiver: stale timestamp and rotated secret
Two tests every receiver needs: a delivery older than 300 seconds must be refused, and a header with two sume-v1 entries must verify when either secret matches.

Write two tests. First, sign a delivery with a timestamp older than 300 seconds and expect your receiver to refuse it. Second, build a header with two sume-v1 entries, newest first, signed with a new and an old secret, and expect the receiver to accept it when either secret matches. A receiver that passes both is correct for the replay window and for a rotation.
These two tests catch the bugs that a happy path test never shows. A receiver that checks only the signature accepts a recorded request forever, and a receiver that compares the whole header to one expected string breaks the moment a rotation starts.
What the receiver must enforce
Sume signs <timestamp>.<raw_body> with HMAC-SHA256 and sends x-sume-webhook-timestamp with the signature in x-sume-webhook-signature as sume-v1=<hex>. The docs advise to reject callbacks when the timestamp is outside your replay tolerance, and they call five minutes a reasonable default.
The replay window protects you from an attacker who has a valid recorded delivery. The signature covers the timestamp, so the attacker cannot change it without breaking the HMAC, and the window makes the old one useless. Five minutes is the default of the SDK and the value the docs suggest as reasonable.
The stale timestamp test also guards against clock drift. If your server clock is wrong by more than five minutes, every real delivery will fail the check, so keep time synchronised on every host that runs the receiver.
What a rotation does to the header
During a rotation, which lasts 24 hours, Sume signs with both secrets and sends the entries separated by a comma, newest first. A receiver that holds either secret can verify. After the window the old secret stops working. The fingerprint header names the new secret from the moment you rotate.
Plan the order of a rotation around this behaviour. Upgrade the receiver so it accepts a header with several entries, then rotate, then roll out the new secret to every receiver within the 24 hours, and let the old entry expire. If you rotate twice inside one window, Sume retires the secret from two rotations before at once, which is how you make a leak really stop.
Both tests in one file
The tests below use only the standard library and a small verifier. They sign their own fixtures, so they run offline and need no Sume key.
Notice the order in the verifier: it checks the timestamp first, and computes the HMAC only for a fresh request. This keeps a flood of stale or garbage requests cheap. It also means that your test for a stale timestamp passes even if the signature is perfect, which is the point of the test.
Each assertion in the sample maps to one rule: a stale timestamp is refused, the new secret verifies a two entry header, the old secret verifies it too, and an unrelated secret does not. The empty secret check at the top means a missing environment variable crashes the service at the first request, not later, and it never lets a request through unverified.
import hashlib, hmac, time
def sign(secret, ts, body):
return "sume-v1=" + hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
def verify(secret, ts, header, body, tolerance=300):
if not secret:
raise RuntimeError("empty webhook secret")
if abs(time.time() - ts) > tolerance:
return False
want = sign(secret, ts, body)
return any(hmac.compare_digest(e.strip(), want) for e in header.split(","))
body, now = b'{"event":"job.completed"}', int(time.time())
old, new = "whsec_old", "whsec_new"
stale = now - 301
assert verify(new, stale, sign(new, stale, body), body) is False
both = sign(new, now, body) + "," + sign(old, now, body)
assert verify(new, now, both, body) and verify(old, now, both, body)
assert verify("whsec_other", now, both, body) is False
print("all webhook checks passed")
Test the route, not only the function
Run the same cases against your real route in an integration test, with a fake clock if your framework supports one. Do not test only the verifier function while the route does its own parsing, because the common production bug is a framework that re-serializes the JSON body before the check. A test that posts the raw bytes through the route catches it.
Add the same two cases to your CI suite and run them against the real handler, not against a copy of the verifier. Then a refactor that drops the window check or the multi-signature support turns the build red.
Sources
Related posts
More in Developers
- Test a video poll loop with a fake clock, no real waits (Python)
Inject sleep and clock into your Sume job poller so a test of a 20-minute deadline runs in milliseconds. Includes a stdlib-only script that passes.
- Text-to-image-only models on Sume reject input_references
Five Sume image ids take no reference images: Soul, Imagen 4 Fast and Ultra, Recraft V4 and Qwen Image Max. The error you get and a Python guard before sending.
- Text to speech API with emotion: audition four values, keep the winner
Sume TTS 1.0 takes a free-text emotion up to 64 characters in generation_config. Run a four-take audition for 4 cents and store the winner.
- Text-to-video API in Node: submit, poll and download with a deadline
A runnable Node 18+ script that submits a text-to-video job to Sume, polls it with a deadline, and saves the MP4. No dependencies and no top-level await.
Written by Sume