Python verifier for Sume webhooks: stale timestamps, empty secret
A short Python function that verifies a Sume webhook: five-minute timestamp window, constant-time HMAC compare and a hard refusal of an empty secret.

Sume signs each webhook with HMAC-SHA256 over <timestamp>.<raw_body> and sends it as sume-v1=<hex> in x-sume-webhook-signature, with the Unix time in x-sume-webhook-timestamp. A safe verifier does four things: refuses to start with an empty secret, rejects a timestamp more than five minutes from now, hashes the raw bytes (never re-serialised JSON), and compares in constant time. Below is a complete function that runs as written, with a self-test.
The scheme comes from Runs and results and Run webhooks; the cookbook has the FastAPI route that wraps it.
The verifier
Put the check in a function that takes the raw bytes and two header values. The web framework is not involved, so it is easy to unit test.
import hashlib
import hmac
import os
import time
TOLERANCE_SECONDS = 300
def verify(secret: bytes, raw: bytes, timestamp: str | None, signature: str | None) -> bool:
if not secret:
raise ValueError("empty webhook signing secret")
if not timestamp or not signature:
return False
try:
ts = int(timestamp)
except ValueError:
return False
if abs(time.time() - ts) > TOLERANCE_SECONDS:
return False
digest = hmac.new(secret, f"{ts}.".encode() + raw, hashlib.sha256).hexdigest()
return hmac.compare_digest(f"sume-v1={digest}", signature)
if __name__ == "__main__":
secret = os.environ.get("SUME_COM_WEBHOOK_SIGNING_SECRET", "test-secret").encode()
body = b'{"event":"format.run.terminal"}'
now = str(int(time.time()))
good = "sume-v1=" + hmac.new(secret, f"{now}.".encode() + body, hashlib.sha256).hexdigest()
print(verify(secret, body, now, good))
print(verify(secret, body, str(int(time.time()) - 900), good))What each line protects against
The table maps each check to the failure it prevents. None of them is optional.
| Check | Failure it prevents |
|---|---|
| Empty secret raises | An unset env var that makes every signature computable by anyone |
| Timestamp window | Replay of an old captured delivery |
| Raw bytes | A parser that reorders keys and breaks the signature |
compare_digest | Timing leaks when comparing signatures |
Around the function
Read the secret once at start-up and fail fast if it is missing. Return 401 on a failed check, 2xx within 10 seconds on success, and dedupe on request_id since retries repeat it. A run can send up to 10 attempts, so your handler must tolerate repeats.
- Use the dashboard's Webhooks tab or
GET /v1/webhooks/signing-secretto read the secret. - Compare the fingerprint header with the one on the dashboard.
- Test with
POST /v1/webhooks/test-deliveriesbefore a real run.
Redelivery for tests
After you fix a receiver, POST /v1/format-runs/{run_id}/webhook/redeliver re-sends the current receipt with a fresh timestamp and signature, without using one of the automatic ten attempts. That makes your tolerance check easy to exercise on a real payload.
Sources
Related posts
More in Developers
- Forward verified Sume webhooks to Kafka, keyed by job_id
Verify the sume-v1 signature, refuse an empty secret, then produce each job event to Kafka with job_id as the key so retries land in one partition.
- Gemini 3.8 Live audio: wrap 24 kHz PCM in WAV, resample to 16 kHz
Gemini 3.8 Live takes 16-bit 16 kHz PCM in and returns 24 kHz out. A Python WAV wrapper, an ffmpeg resample command, and the Sume detach settings that match.
- New model id on a Sume Format run: probe for a 400 before launch day
When Gemini 4 Argon or any new model opens up, test whether a Format run accepts its id. A 400 invalid_request means it is not in the catalog yet.
- Gemini video understanding 88% fewer tokens vs Sume Video inspect
Gemini reports up to 88% fewer tokens on long video. Sume Video inspect and Reference ingest take another route: stills, transcript and a manifest.
Written by Sume