Verify a Sume Format webhook in Python: empty secret, stale timestamp
A Python check for the Sume format.run.terminal webhook: HMAC-SHA256 over timestamp.raw_body, a 5-minute window, and a refusal to run with an empty secret.

Verify a Sume Format webhook by computing HMAC-SHA256 over <timestamp>.<raw_body> with your signing secret and comparing it, in constant time, to the sume-v1= value in x-sume-webhook-signature. Reject a timestamp more than five minutes off, and refuse to start at all if the secret is empty, since an empty key makes every forgery valid.
What arrives
A run with communication.webhook_url gets one signed format.run.terminal POST when it completes or fails. Two headers matter.
| Item | Value |
|---|---|
x-sume-webhook-timestamp | Unix seconds, as a string |
x-sume-webhook-signature | sume-v1= followed by the hex digest |
| Signed string | <timestamp>.<raw_body> |
| Algorithm | HMAC-SHA256 |
| Tolerance | 300 seconds |
The check
Sign the raw bytes, not a re-serialized JSON object. A framework that parses first and dumps again changes whitespace and key order, and the digest no longer matches.
import hashlib, hmac, os, time
SECRET = os.environ.get("SUME_WEBHOOK_SECRET", "")
if not SECRET:
raise SystemExit("SUME_WEBHOOK_SECRET is empty: refusing to verify")
TOLERANCE_SECONDS = 300
def verify(raw: bytes, timestamp: str | None, signature: str | None) -> bool:
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
signed = timestamp.encode() + b"." + raw
digest = hmac.new(SECRET.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest("sume-v1=" + digest, signature)
if __name__ == "__main__":
body = b'{"type":"format.run.terminal"}'
now = str(int(time.time()))
good = "sume-v1=" + hmac.new(SECRET.encode(), now.encode() + b"." + body, hashlib.sha256).hexdigest()
print(verify(body, now, good), verify(body, now, "sume-v1=00"))Why these checks
A webhook endpoint is a public URL, so anyone can post to it. The signature proves the body came from Sume and the timestamp proves it is recent. Without both, a forged or replayed format.run.terminal could mark a job done in your system when it never ran.
Failure modes
Each guard closes a specific hole.
- Empty secret: the process exits at start instead of accepting any signature.
- Missing or non-numeric timestamp: returns false before any hashing.
- Stale timestamp: a captured request cannot be replayed after five minutes.
compare_digest: the comparison takes the same time whether the first byte or the last differs.
Around the check
Return a 401 when verify is false and a 2xx quickly when it is true, then do the work off the request. Keep the run's result_url as a backup, since the webhook is a push and a poll recovers a missed one. Delivery attempts are capped (the receipt shows max_attempts of 10), and the receipt shows webhook_delivery with max_attempts and status, so a receiver that was down can be seen from the run itself.
The queue object of a bulk run has no webhook. Register communication.webhook_url on each child item instead.
Testing the receiver
Test the function before you point a real run at it. The main block in the sample signs a body with the current timestamp and checks that a correct signature passes and a wrong one fails. Add two more cases in your own tests: a timestamp 301 seconds old, which must fail even with a correct digest, and the same body with one byte changed, which must fail too.
Use the raw request body in your web framework. In most frameworks that means reading bytes before any JSON parser touches the request. If your framework gives you only parsed JSON, add a raw-body hook rather than re-serializing.
Secrets and rotation
Store the signing secret in your secret manager and load it into the environment at deploy time. Never commit it. When you rotate the secret, update the receiver first and then the sender, and watch for 401 responses during the switch. The refuse-at-start check helps here: a deploy that lost the variable fails loudly instead of accepting everything.
Sources
Related posts
More in Developers
- Verify a Sume avatar video webhook signature in Python
A Python verifier for Sume job webhooks: HMAC SHA-256 over timestamp.raw_body, sume-v1 entries, a 5-minute window, and a refusal when the secret is empty.
- Verify a Sume agent.run.terminal webhook in Python
Python HMAC-SHA256 check for a Sume run webhook: sume-v1 signature over timestamp.raw_body, a five-minute window, and a verifier that refuses an empty secret.
- Verify a Sume image webhook in Python, then read artifacts[]
A 23-line Python verifier for Sume's x-sume-webhook-signature header on an image job: refuses an empty secret, checks a 5-minute window, reads the image URL.
- Verify the Sume signature in the HTTP handler, not in the queue worker
A queue delay over 300 seconds makes a valid Sume signature look stale. Verify at receipt, enqueue the verified event, and use redeliver if a late check failed.
Written by Sume