Sign a test job.completed webhook and replay it at your receiver
Build a Sume-style job.completed delivery in Python: HMAC over timestamp.body, a verifier that refuses an empty secret, plus tamper, stale and rotation checks.

You can test a Sume webhook receiver without waiting for a real render by signing a payload yourself. The signature is sume-v1= followed by the hex HMAC-SHA256 of the timestamp, a dot and the raw body, sent with an x-sume-webhook-timestamp header. The script below signs, verifies, and proves that tampering, a stale timestamp and an empty secret are all handled.
What a delivery looks like
Test the unhappy paths first. A receiver that accepts a good signature is easy; the bugs live in the body that changed by one byte, the timestamp from yesterday and the secret that was never configured. The asserts above exist to make each of those a failing test in your own suite.
| Item | Value |
|---|---|
| Event names | job.completed, job.failed, job.canceled |
| Payload keys | event, request_id, job_id, status (OK or ERROR), payload.artifacts |
| Timestamp header | x-sume-webhook-timestamp |
| Signature header | x-sume-webhook-signature: sume-v1=<hex> |
| Signed string | <timestamp>.<raw_body> |
| Secret rotation | Header may carry comma-separated entries, any match is accepted |
| Suggested replay window | 5 minutes |
Sign the raw body
Sign the exact bytes you will send, and verify against the raw body, never a re-serialized one. A parser that reorders keys changes the bytes and breaks the signature.
Sign and verify
Run this file as it stands. The payload type video is illustrative: only the field layout comes from the webhook docs. Every assert is a case your production receiver should reject or accept the same way.
The empty-secret check is deliberate. If an environment variable is missing, an HMAC with an empty key still produces a valid-looking signature, and an attacker who knows that can forge deliveries. Raising an error at startup is safer than quietly accepting everything.
import hashlib, hmac, json, time
def sign(secret, body, ts=None):
ts = str(ts or int(time.time()))
mac = hmac.new(secret.encode(), f"{ts}.{body}".encode(), hashlib.sha256).hexdigest()
return {"x-sume-webhook-timestamp": ts, "x-sume-webhook-signature": f"sume-v1={mac}"}
def verify(secret, body, headers, tolerance=300):
if not secret:
raise ValueError("refusing to verify with an empty secret")
ts = headers.get("x-sume-webhook-timestamp", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > tolerance:
return False
want = hmac.new(secret.encode(), f"{ts}.{body}".encode(), hashlib.sha256).hexdigest()
entries = [e.strip() for e in headers.get("x-sume-webhook-signature", "").split(",")]
return any(hmac.compare_digest(e, f"sume-v1={want}") for e in entries)
body = json.dumps({"event": "job.completed", "request_id": "job_1", "job_id": "job_1", "status": "OK",
"payload": {"artifacts": [{"id": "artf_1", "url": "https://media.sume.com/artifacts/x.mp4",
"type": "video", "content_type": "video/mp4"}]}})
good = sign("whsec_test", body)
assert verify("whsec_test", body, good)
assert not verify("whsec_test", body + " ", good) # body changed
assert not verify("whsec_test", body, sign("whsec_test", body, ts=1)) # stale
rotated = {**good, "x-sume-webhook-signature": "sume-v1=00," + good["x-sume-webhook-signature"]}
assert verify("whsec_test", body, rotated) # rotation: any entry matches
try:
verify("", body, good)
except ValueError:
print("ok")Replay with curl
To replay against your running receiver, print the two header values from sign() and send the same body bytes with curl. Set TS, SIG and BODY from that output first.
curl -sS -X POST http://localhost:8000/hooks/sume \
-H "x-sume-webhook-timestamp: $TS" \
-H "x-sume-webhook-signature: $SIG" \
-H "Content-Type: application/json" \
--data-binary "$BODY"Before you ship
Dedupe on job_id, since deliveries can repeat. The webhook docs cover the rotation header and the redeliver route, and the receiver sizing post explains why the handler should answer fast.
After the local test passes, use the redeliver route on a real finished job to confirm the receiver also works with Sume's actual headers, including the x-sume-webhook-secret-fingerprint header.
Sources
Related posts
More in Developers
- Sign and verify a Sume webhook offline: a Python test vector
Build a sume-v1 test signature with Python's hmac and check your verifier against it, including an empty secret, an old timestamp and a rotation header.
- size vs image_size vs aspect_ratio: which field wins on Sume images
On POST /v1/images, image_size beats aspect_ratio, size is a tier word that rejects WxH, and resolution is a tier. Which one to send per model.
- Sora's GET /videos and DELETE /videos/{id}: what Sume offers instead
Sora had list and delete routes. Sume lists your key's own jobs with GET /v1/jobs and documents no public delete, so plan retention in your own storage.
- Sora to Sume in Python: a 10% rollout flag with a spend guard
Move video traffic off a dead Sora call one slice at a time. A stable per-user percentage flag, one Sume call, and a cost guard that stops at your daily cap.
Written by Sume