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.

5 min readSume
All posts

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.

Webhook shapes, read 2026-10-02
Mux Robots statusSume eventTerminal
pendingjob.queued appears in job events, not as a webhookNo
processingnone sentNo
completedjob.completedYes
erroredjob.failedYes
cancelledjob.canceledYes

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 ok

What 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_id as 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

All Developers posts

Written by Sume