Sume webhook timestamp window: reject stale deliveries (Python)
The Sume signature covers a timestamp, and the SDK default rejects anything over 300 seconds old. Verify at the edge, then queue, so a late worker never fails.

A Sume job webhook is signed over <timestamp>.<raw_body> with HMAC-SHA256, and the timestamp travels in the x-sume-webhook-timestamp header. That timestamp is not decoration. The SDK's verifyWebhook rejects a delivery when the timestamp is more than 300 seconds from the receiver's clock, and it treats a tolerance of 0 as "skip the check". If you write your own verifier, the window is yours to enforce.
The trap is where you enforce it. Teams that push the raw request onto a queue and verify in a worker find that a backlog of ten minutes turns every signed delivery into a failure, even though nothing was forged. Verify at the edge, in the HTTP handler, before you enqueue anything. Store the parsed event, not the headers.
The verifier
This function uses only the Python standard library. It refuses an empty secret, a missing header, a non-numeric timestamp and a stale timestamp, then compares every sume-v1= entry in the header. During a secret rotation Sume can send two comma-separated entries, so a loop that compares all of them keeps both the old and the new secret working.
import hashlib, hmac, time
TOLERANCE = 300 # seconds, the SDK default
def verify(body: bytes, headers: dict, secret: str, now=time.time) -> bool:
if not secret:
return False # never verify against an empty secret
h = {k.lower(): v for k, v in headers.items()}
sig, ts = h.get("x-sume-webhook-signature"), h.get("x-sume-webhook-timestamp")
if not sig or not ts:
return False
try:
sent = int(ts)
except ValueError:
return False
if abs(now() - sent) > TOLERANCE:
return False # stale: reject before queueing
mac = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256)
want = "sume-v1=" + mac.hexdigest()
ok = False
for part in sig.split(","): # rotation can send two entries
ok |= hmac.compare_digest(part.strip(), want)
return okTest it without a network
The now argument lets a test move the clock instead of sleeping. The block below signs a body, then checks four cases: a fresh delivery passes, a 301-second-old delivery fails, an empty secret fails, and a body that changed by one byte fails. Run it after the verifier above and it prints ok.
def sign(body, secret, ts):
mac = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256)
return {"X-Sume-Webhook-Timestamp": str(ts),
"X-Sume-Webhook-Signature": "sume-v1=" + mac.hexdigest()}
body, secret, t = b'{"event":"job.completed"}', "whsec_test", 1_000_000
assert verify(body, sign(body, secret, t), secret, now=lambda: t + 10)
assert not verify(body, sign(body, secret, t), secret, now=lambda: t + 301)
assert not verify(body, sign(body, secret, t), "", now=lambda: t)
assert not verify(body + b" ", sign(body, secret, t), secret, now=lambda: t)
print("ok")What the window does and does not cover
- It limits replay of a captured request to 300 seconds. It does not make a replay harmless inside that window, so keep a dedupe table keyed on
job_id. - It depends on your clock. If a host drifts by more than five minutes, valid deliveries fail. Run NTP and alert on 401s from this route.
- Redelivery from
POST /v1/jobs/{job_id}/webhook/redeliversends a fresh timestamp, so recovering from a late worker does not need a wider window. - Do not widen the tolerance to hide a slow queue. Move the check earlier instead.
Keep polling as a backup
Webhooks tell you a job ended. They are not the only record. The jobs guide recommends keeping the status URL as a fallback, and a stale-timestamp rejection is exactly the case where a poll of /v1/jobs/{id}/status recovers the result without anyone resending anything. For the delivery rules themselves, including the terminal-only events and the signature headers, read the webhooks page.
Sources
Related posts
More in Developers
- Sume run webhook behind a redirect: a 3xx counts as a failed attempt
Sume does not follow redirects on run webhooks, so a 301 from an http or www hostname is a failed delivery. The URL is also re-checked at delivery time.
- Supabase Realtime for Sume render progress, with RLS per subscriber
Add the job table to the supabase_realtime publication and subscribe with a row filter. Realtime checks RLS for every subscriber, so keep the table lean.
- SvelteKit +server.js endpoint to verify a Sume webhook signature
A SvelteKit +server.js POST handler gets a Fetch Request, so request.text() gives the raw body Sume signs. Verify the HMAC, then handle job.completed.
- Swap the TTS engine, keep the voice: Sonic 3.5 to 3.6 on Sume
Cartesia treats the TTS model and the voice as separate things. On Sume the model id and voice id are separate fields, so you can A/B 3.5 and 3.6 on one voice.
Written by Sume