One Python verifier for Sume job and run webhooks, routed on event
Job webhooks and run webhooks share one signing secret and one signature scheme, so one verify function plus a router on the event field covers both.

You need one signature check for every Sume webhook, because job webhooks and run webhooks sign the same way with the same workspace secret. What differs is the event name, so route on event after the check passes.
The Webhooks page says job webhooks and run webhooks share that one secret, so a single verifier covers both. Splitting them into two code paths is the usual way a receiver ends up verifying one kind and silently rejecting the other.
Two surfaces, one scheme
Which events you get depends on what you called. A generation model endpoint sends job events; an Action, Format or Agent Completion run endpoint sends run events (read 2026-10-03).
| You called | Events | Identifier to dedupe on |
|---|---|---|
| A model endpoint such as POST /v1/avatar-1.0/generate | job.completed, job.failed, job.canceled | job_id |
| A Format run endpoint | format.run.terminal | run_id (request_id repeats it) |
| An Action run endpoint | action.run.terminal | run_id |
| An Agent Completion run | agent.run.terminal | run_id |
The verify function and the router
The signature is HMAC-SHA256 over <timestamp>.<raw_body>, sent as x-sume-webhook-signature: sume-v1=<hex>, with the timestamp in x-sume-webhook-timestamp. During a secret rotation the header holds one entry per live secret, newest first, so the function compares every entry. It returns False for an empty secret rather than signing with an empty key.
The block below is self-contained: it signs a sample body, verifies it with a stale old entry in front, checks the empty-secret refusal and routes the event. It prints True, False, then the routed tuple.
import hashlib, hmac, json, time
def verify(secret, raw, ts, header, tolerance=300):
if not secret or not ts.isdigit() or abs(time.time() - int(ts)) > tolerance:
return False
mac = hmac.new(secret.encode(), ts.encode() + b"." + raw, hashlib.sha256)
want = "sume-v1=" + mac.hexdigest()
return any([hmac.compare_digest(e.strip(), want) for e in header.split(",")])
def route(event):
kind = event["event"]
if kind.startswith("job."):
return ("job", event["job_id"], kind.split(".")[1])
if kind.endswith(".run.terminal"):
return ("run", event["run_id"], event.get("outcome"))
return ("ignored", None, kind)
secret, raw = "s3cret", b'{"event":"format.run.terminal","run_id":"r1","outcome":"ok"}'
ts = str(int(time.time()))
sig = "sume-v1=" + hmac.new(secret.encode(), ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
old = "sume-v1=" + "0" * 64
print(verify(secret, raw, ts, f"{old},{sig}"))
print(verify("", raw, ts, sig))
print(route(json.loads(raw)))Using it in a handler
In a web framework, pass await request.body() or the equivalent raw bytes as raw, never a re-serialized dict, because re-encoding changes whitespace and key order and the HMAC no longer matches. Reject with 401 when verify is False, store the event, then answer 2xx.
Treat the route result as a hint, not a contract: unknown events fall into ignored, so a new event type does not crash the receiver. When a signature fails that you expected to pass, compare x-sume-webhook-secret-fingerprint with the fingerprint beside the secret in the dashboard, as described in compare the secret fingerprint.
Sources
Related posts
More in Developers
- One image set for Chrome Web Store and Edge Add-ons: sizes
Both stores list a 440 x 280 small tile and a 1400 x 560 marquee. Edge adds 640 x 480 and 1280 x 800 screenshots, so one Sume master set can serve both.
- One request for four Sume video models: 1080p, 5 to 10 s, 16:9 or 9:16
Kling 3, Wan 3.0, H3 Max and Omni share one request shape: 1080p, 5 to 10 seconds, 16:9 or 9:16. Compute the intersection in Python and price each model.
- OpenAI Agents SDK client_session_timeout_seconds with Sume jobs_wait
In the OpenAI Agents SDK for Python, client_session_timeout_seconds sets the MCP read timeout. Set it above Sume's 55-second jobs_wait cap, or 0 to disable it.
- OpenAI Agents SDK max_retry_attempts and Sume paid calls
The OpenAI Agents SDK can retry MCP list_tools and call_tool. A retried Sume create must repeat its idempotency_key or you pay twice; set max_spend_usd too.
Written by Sume