Python webhook receiver: read artifacts[].url and error.next_action

A standard-library Python receiver for Sume job webhooks: verify, then branch on status OK or ERROR to get the artifact URL or error.retryable and next_action.

4 min readSume
All posts

Signature checking gets most of the attention in webhook posts, but the part you actually ship is what happens after it passes. A Sume job webhook has two shapes that need different code, and both are keyed on the top-level status field.

For a completed job, event is job.completed, status is OK and payload.artifacts is an array of {id, url, type, content_type}. For a failed or canceled job, status is ERROR, payload is null and an error object takes its place. The request_id and job_id fields hold the same job id.

The two shapes

Sume job webhook payload by outcome, from the webhook guide and API source (read 2026-10-03)
Eventstatuspayloaderror
job.completedOKartifacts[] with id, url, type, content_typeabsent
job.failedERRORnullobject with code, message, retryable, next_action
job.canceledERRORnullobject

A receiver that handles both

This standard-library receiver verifies the signature over the raw bytes first, including the 300 second window, and returns 401 for anything that fails. Only then does it parse the JSON and call handle. For OK it prints the first artifact URL. For ERROR it prints the error code, whether it is retryable and the next_action hint, which is the field to branch on in real code.

import hashlib, hmac, json, os, time
from http.server import BaseHTTPRequestHandler, HTTPServer

SECRET = os.environ.get("SUME_WEBHOOK_SECRET", "")
if not SECRET:
    raise SystemExit("SUME_WEBHOOK_SECRET is required")

def valid(raw: bytes, h) -> bool:
    ts, sig = h.get("x-sume-webhook-timestamp", ""), h.get("x-sume-webhook-signature", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        return False
    mac = hmac.new(SECRET.encode(), ts.encode() + b"." + raw, hashlib.sha256)
    return any(hmac.compare_digest(p.strip(), "sume-v1=" + mac.hexdigest()) for p in sig.split(","))

def handle(event: dict) -> str:
    if event["status"] == "OK":
        return f"{event['job_id']}: download {event['payload']['artifacts'][0]['url']}"
    err = event.get("error") or {}
    return f"{event['job_id']} {event['event']}: {err.get('code')} retryable={err.get('retryable')} next={err.get('next_action')}"


class Hook(BaseHTTPRequestHandler):
    def do_POST(self):
        raw = self.rfile.read(int(self.headers.get("content-length", 0)))
        ok = valid(raw, {k.lower(): v for k, v in self.headers.items()})
        if ok:
            print(handle(json.loads(raw)))
        self.send_response(200 if ok else 401)
        self.end_headers()

Run it

Append the self-test. It starts the receiver on a free port, then sends one signed completed event and one signed failed event, and prints the HTTP status each time. Set SUME_WEBHOOK_SECRET to any test value and run the file with Python 3. The two handle lines print before their 200 lines. The media_invalid code in the sample is a placeholder, so use the codes in your own logs.

import threading, urllib.request

server = HTTPServer(("127.0.0.1", 0), Hook)
threading.Thread(target=server.serve_forever, daemon=True).start()
events = [
    {"event": "job.completed", "job_id": "job_a", "status": "OK",
     "payload": {"artifacts": [{"url": "https://example.com/out.mp4"}]}},
    {"event": "job.failed", "job_id": "job_b", "status": "ERROR", "payload": None,
     "error": {"code": "media_invalid", "retryable": False, "next_action": "fix_input"}},
]
for e in events:
    raw, ts = json.dumps(e).encode(), str(int(time.time()))
    sig = hmac.new(SECRET.encode(), ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
    req = urllib.request.Request(f"http://127.0.0.1:{server.server_port}/", raw, {
        "x-sume-webhook-timestamp": ts, "x-sume-webhook-signature": "sume-v1=" + sig})
    print(urllib.request.urlopen(req).status)

What to do with the result

  • Store the event and the artifact URL, return 200, then download in a worker. Do not hold the delivery open while you fetch a video.
  • On ERROR, read error.retryable before you resubmit, and resubmit with the same Idempotency-Key when it is true.
  • Treat next_action as a hint: fix_input means change the request, retry_later means wait, contact_support means send the request id.
  • Deduplicate on job_id and event. A redelivery can reach you more than once.

Payload fields, retries and redelivery are in the webhook guide. The meaning of each error field is in the error reference.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume