Mux Robots webhook events vs Sume job events: a verifier
Mux sends robots.job.{workflow}.{status} on every change. Sume sends only job.completed, job.failed and job.canceled, signed. Verify them in Python.

Mux Robots sends a webhook on every job status change, named robots.job.{workflow}.{status}, while Sume sends terminal events only: job.completed, job.failed and job.canceled. If you are moving a handler from one to the other, expect fewer, signed deliveries and keep a polling fallback.
Mux's side comes from its Robots API guide; Sume's comes from Webhooks and Jobs and results.
What events does each side send?
Per the Mux guide, jobs move through pending, processing, completed, errored or cancelled, and every status change triggers a webhook following robots.job.{workflow}.{status}. Multi-word workflows use underscores, for example robots.job.find_key_moments.completed. Your access token needs the robots:* scope.
Sume's docs say it sends terminal job events only, with no progress or partial deliveries. You opt in per request with mode: "webhook" and a public HTTPS webhook_url; localhost, private-network and non-HTTPS URLs are rejected.
| Mux Robots status | Sume event | Terminal |
|---|---|---|
| pending | job.queued appears in job events, not as a webhook | No |
| processing | none sent | No |
| completed | job.completed | Yes |
| errored | job.failed | Yes |
| cancelled | job.canceled | Yes |
How does Sume sign a delivery?
When signing is configured, Sume signs the raw JSON body with HMAC SHA-256 over <timestamp>.<raw_body> and sends x-sume-webhook-timestamp plus x-sume-webhook-signature: sume-v1=<hex>. During a secret rotation the header carries one sume-v1= entry per live secret, comma-separated, and you accept the delivery if any entry matches. Reject timestamps outside your tolerance; the docs suggest five minutes.
The Mux guide excerpt I read does not describe a signature scheme, so do not assume your Mux verifier carries over. Read your Sume signing secret from the dashboard Webhooks tab or GET /v1/webhooks/signing-secret.
What is a safe Python verifier?
This one refuses an empty secret, because an empty key would make every forged body verify, and compares every entry in constant time.
import hashlib
import hmac
import time
def verify(raw: bytes, ts: str, header: str, secret: str, tol: int = 300) -> bool:
if not secret:
raise ValueError("empty webhook secret refused")
try:
stamp = int(ts)
except ValueError:
return False
if abs(time.time() - stamp) > tol:
return False
body = f"{stamp}.".encode() + raw
digest = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
expected = f"sume-v1={digest}"
ok = False
for entry in header.split(","):
if hmac.compare_digest(entry.strip(), expected):
ok = True
return okWhat should the handler do after it verifies?
Store the event durably, then return any 2xx. Sume retries network errors and non-2xx responses up to 10 attempts total, with a fixed delay (30 seconds by default) and a 10-second timeout per attempt. Use job_id as your idempotency key, because a redelivery carries the same job.
Ten refused attempts leave a failed delivery but a job that still reached its real terminal state, so keep status_url polling for events that never arrive. A manual redeliver is POST /v1/jobs/{job_id}/webhook/redeliver, which re-sends the real terminal event with a fresh timestamp and signature. Action, Format and Agent runs use a different event set, covered in Run webhooks, but the same verifier works for both.
What are the common migration mistakes?
The first is parsing the event name. Mux encodes workflow and status in the event type, so handlers often switch on a string such as robots.job.find_key_moments.completed. Sume's event is just job.completed and the job id is in the body, so look up the job to learn what ran.
The second is hashing a re-serialised body. The signature covers the raw JSON bytes, so verify before you parse; a framework that parses first and re-encodes will change whitespace and break the match. The third is treating a missed webhook as a lost job. The job reached its terminal state anyway, so poll status_url for anything still open after your timeout.
- Verify against the raw bytes, not a re-encoded object.
- Use
job_idas the idempotency key. - Keep polling as a fallback.
- Run Send test from the dashboard to prove your endpoint before a real job.
Sources
Related posts
More in Developers
- Netflix subtitle limit: 42 characters per line, and max_chars
Netflix's English timed text spec allows 42 characters per line and two lines. Sume's caption design.phrasing.max_chars accepts 4 to 60, so 42 fits.
- Subtitle reading speed: check 20 characters per second before burning
Netflix caps English subtitles at 20 characters per second for adults, 17 for children. Check each cue's rate in a short script before a Sume caption render.
- NEXT_PUBLIC_ plus a Sume API key: why it ships to the browser
A NEXT_PUBLIC_ prefix inlines the value into client JavaScript at build time. Keep the Sume API key server-side, proxy via a route handler, rotate if it leaked.
- Poll a Sume job with AbortSignal.any in Node 26.10
Node 26.10.0 fixes AbortSignal.any() propagation. Here is a Sume job poll loop with a hard deadline and a caller cancel, using next_poll_after_seconds.
Written by Sume