Start the trim step from a job.completed webhook: Python verifier
Let Sume's signed job.completed webhook trigger the next chain step. A stdlib Python verifier that refuses an empty secret, plus dedupe on job_id.

To start the trim step when a render finishes, submit the render with mode: "webhook" and a public HTTPS webhook_url, verify the signature on the job.completed delivery, then submit the trim with its own Idempotency-Key and the artifact URL from the payload. Dedupe on job_id, because Sume retries until it gets a 2xx. Keep polling status_url as a backup for deliveries that never arrive.
What arrives
Job webhooks are terminal-only: job.completed, job.failed and job.canceled. There are no progress events. A completed delivery carries event, request_id, job_id, status: "OK" and payload.artifacts[], each with a url on media.sume.com. Sume signs <timestamp>.<raw_body> with HMAC SHA-256 and sends x-sume-webhook-timestamp and x-sume-webhook-signature: sume-v1=<hex>. During a secret rotation the header holds several comma-separated entries and any match is valid.
| Fact | Value |
|---|---|
| Events | job.completed, job.failed, job.canceled |
| Attempts | Up to 10, fixed 30 s delay by default |
| Timeout per attempt | 10 s |
| Replay tolerance (suggested) | 5 minutes |
| Dedupe key | job_id |
Verifier that refuses an empty secret
The docs publish a TypeScript verifier. This is the same check in standard-library Python. It returns False for an empty secret, so a misconfigured deploy rejects deliveries instead of accepting anything.
import hashlib, hmac, time
def verify(raw: bytes, ts: str, header: str, secret: str, tol: int = 300) -> bool:
if not secret:
return False
try:
t = int(ts)
except ValueError:
return False
if abs(time.time() - t) > tol:
return False
digest = hmac.new(secret.encode(), f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
want = f"sume-v1={digest}"
ok = False
for entry in header.split(","):
if hmac.compare_digest(entry.strip(), want):
ok = True
return okTrigger the next step safely
Verify against the raw request bytes, before any JSON parsing. Store the event durably and return a 2xx, then act. Insert job_id into a table with a unique constraint, and run the trim submit only when the insert is new. The trim key should be derived from your chain id, for example order-8823-trim, so that a second delivery of the same event cannot create a second trim job even if your dedupe row is lost.
If job.failed arrives, branch on it: there is no artifact to trim. Read the job record for its public error, and decide whether a new render is worth paying for. job.canceled also arrives as a delivery for jobs, but run webhooks do not send one for canceled runs, so do not copy a run handler's assumptions here.
Keep the poll fallback
A failed delivery does not mean a failed job. After ten refused attempts the job still reached its real terminal state, and the docs say delivery is an optimization and never the only recovery path. Keep a sweep that reads status_url for chain steps that have been open longer than your retry window.
Check list before you deploy the handler
A receiver is part of your security surface, so test it with the cases that break. Send an empty secret and make sure the verifier refuses to start. Send a body with a changed byte and check that the comparison fails. Send an old timestamp and check that your replay window rejects it. Send the same valid event twice and check that only one trim is submitted.
Respond quickly. Sume gives each delivery a 10 second timeout and makes up to 10 attempts with a fixed 30 second gap, so a handler that does the trim submit inline and takes too long will see retries of the same event. Acknowledge first, then do the work, and let the job_id dedupe absorb any retries.
Keep the signature check over the raw body. Parsing the JSON and serializing it again changes bytes and breaks the HMAC.
- Refuse an empty secret.
- Verify over
<ts>.<raw_body>. - Dedupe on
job_id. - Answer within the 10 second timeout.
Sources
Related posts
More in Developers
- Job id or run id: which Sume endpoint and helper do you poll?
A job_ id is polled at /v1/jobs/:id with waitForJob. An arun_ id is a Format, Action or Agent run, polled with waitForRun and a family argument.
- job_id, request_id, Idempotency-Key: which one goes in which column
Your Idempotency-Key is the unique key before submit, job_id or run_id is the poll key, and request_id dedupes webhooks and goes into support tickets.
- 503 status_busy on GET /v1/jobs/{id}/status: back off and jitter
status_busy means Sume's job status read gate is full. Reads of the same job are shared; the cap is 100 distinct in-flight reads. Poll slower, add jitter.
- Sume jobs_wait outcome: wait_slice_expired is not a failed job
jobs_wait returns outcome terminal, wait_slice_expired or operator_stopped. Only terminal means the jobs ended; an expired slice says nothing about the jobs.
Written by Sume