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.

5 min readSume
All posts

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.

Delivery facts a receiver depends on (read 2026-10-05)
FactValue
Eventsjob.completed, job.failed, job.canceled
AttemptsUp to 10, fixed 30 s delay by default
Timeout per attempt10 s
Replay tolerance (suggested)5 minutes
Dedupe keyjob_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 ok

Trigger 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

All Developers posts

Written by Sume