Python handler for the Sume agent.run.terminal webhook
Verify the HMAC over timestamp.raw_body, refuse an empty secret, dedupe on request_id and branch on outcome. Runs offline with a self-test.

An agent that calls a workflow step and waits on a webhook needs a handler that does four things in order: refuse to run with an empty secret, verify the signature over the raw body, drop duplicates, and branch on outcome. Sume signs run webhooks with HMAC-SHA256 over <timestamp>.<raw_body>, so the handler must hash the bytes it received, not a re-serialized copy of the JSON.
Delivery details are in Sume's Run webhooks; the event for Agent Completions is agent.run.terminal, described in Agent Completions.
What does Sume send?
Each delivery carries x-sume-webhook-timestamp and x-sume-webhook-signature: sume-v1=<hex>. The envelope has status of OK or ERROR, an outcome of ok, degraded or error, and a request_id equal to the run id. The replay window is five minutes. Sume makes up to 10 attempts with a 10-second timeout and does not follow redirects, so answer with a 2xx fast and do the work after. A payload over 1 MiB arrives with payload: null and a result_url.
| Check | Fail action |
|---|---|
| Secret is empty | Raise at startup |
| Timestamp older than 5 minutes | Reject with 400 |
| Signature mismatch | Reject with 401 |
request_id seen before | Return 200, skip work |
outcome is degraded or error | Route to review, not to publish |
What does the code look like?
The secret comes from the dashboard or GET /v1/webhooks/signing-secret; keep it as SUME_COM_WEBHOOK_SIGNING_SECRET. This sample includes a self-test, so it runs without a network.
import hashlib, hmac, json, time
def verify(secret, ts, sig, raw, now=None):
if not secret:
raise ValueError('empty webhook secret')
if abs((now or time.time()) - int(ts)) > 300:
return False
mac = hmac.new(secret.encode(), ts.encode() + b'.' + raw, hashlib.sha256).hexdigest()
return hmac.compare_digest('sume-v1=' + mac, sig)
seen = set()
def handle(secret, ts, sig, raw):
if not verify(secret, ts, sig, raw):
return 'rejected'
body = json.loads(raw)
if body['request_id'] in seen:
return 'duplicate'
seen.add(body['request_id'])
return 'review' if body['outcome'] != 'ok' else 'publish'
raw = json.dumps({'request_id': 'r1', 'outcome': 'ok'}).encode()
ts = str(int(time.time()))
sig = 'sume-v1=' + hmac.new(b's', ts.encode() + b'.' + raw, hashlib.sha256).hexdigest()
print(handle('s', ts, sig, raw), handle('s', ts, sig, raw))What are the common mistakes?
- Parsing the JSON and re-serializing it before hashing. Use the exact bytes.
- Treating
statusOK as success. A degradedoutcomestill needs a look. - Waiting for a webhook on a canceled run. A canceled run delivers none, so poll its status.
- Keeping the dedupe set only in memory in production. Use a store that survives a restart.
What about the official SDK?
@sume-com/sdk ships verifyWebhook for Node. Use it if your handler is JavaScript; the Python above does the same arithmetic for services that are not.
Return the 2xx before slow work such as downloading media or calling another service. Sume waits at most 10 seconds for your response and retries up to 10 times, so a handler that blocks will produce duplicates, which the request_id check then absorbs. Store the id only after the work is queued, not before, so a crash in between leads to a retry instead of a lost event.
Sources
Related posts
More in Developers
- Python: list Sume video models that accept a video input
A 20-line Python script reads GET /v1/videos/models and prints every model whose supported_input_references include video_url, with its duration range.
- Python match on a Sume run status: terminal is not success
A Format run can be terminal as completed, failed, canceled or skipped. A structural match that never treats done as success, with a null-output case.
- Poll a Sume job in Python with a deadline and next_poll_after_seconds
A Python wait loop for a Sume job: stop on terminal, sleep at least next_poll_after_seconds, and give up at a monotonic deadline. A timeout is not a failure.
- Log x-sume-request-id and Idempotency-Key on every call (Python)
A requests response hook that writes one JSON log line per Sume call: x-sume-request-id, idempotency key, error code and rate-limit headers. Tested.
Written by Sume