Forward a finished Sume run to team chat with a signed webhook

A standard-library Python receiver that verifies the sume-v1 signature, rejects an empty secret, and forwards primary_output_url when a Format run ends.

4 min readSume
All posts

To post finished Sume videos into team chat, run a small HTTPS receiver that verifies the signature on each format.run.terminal delivery and then forwards primary_output_url. The signature is HMAC-SHA256 over the timestamp, a dot, and the raw body, sent as sume-v1=<hex>. The receiver below rejects a request when the secret is empty.

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

SECRET = os.environ.get("SUME_COM_WEBHOOK_SIGNING_SECRET", "").encode()


def verify(raw, ts, header):
    if not SECRET or not ts or not header:
        return False
    if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        return False
    mac = hmac.new(SECRET, ts.encode() + b"." + raw, hashlib.sha256)
    want = "sume-v1=" + mac.hexdigest()
    return any(hmac.compare_digest(p.strip(), want) for p in header.split(","))


class Hook(BaseHTTPRequestHandler):
    def do_POST(self):
        raw = self.rfile.read(int(self.headers.get("Content-Length", "0")))
        sig = self.headers.get("x-sume-webhook-signature")
        ts = self.headers.get("x-sume-webhook-timestamp")
        ok = verify(raw, ts, sig)
        if ok:
            run = json.loads(raw)["payload"]
            print(run["id"], run["status"], run.get("primary_output_url"))
        self.send_response(200 if ok else 401)
        self.end_headers()


HTTPServer(("0.0.0.0", 8080), Hook).serve_forever()

What the code does

It reads the raw bytes first, because the signature covers the exact body Sume sent. It returns 401 for a missing header, a stale timestamp or an empty secret. The header can carry more than one sume-v1= entry during a secret rotation, so the check accepts any match.

Delivery rules for run webhooks (Sume docs, read 2026-10-07)
PropertyValue
Signed over<timestamp>.<raw_body>, HMAC-SHA256
Replay windowReject timestamps outside about five minutes
SuccessAny 2xx within 10 seconds
RetriesUp to 10 attempts, backoff to a maximum of one hour
RedirectsNot followed; a 3xx is a failed attempt
Dedupe keyrequest_id, which repeats on retries

From print to chat

Replace the print with a call to your chat tool's incoming-message URL, using the id, the status and primary_output_url. Do the slow work after you answer: write the event to durable storage, return 200, then post. Sume waits only 10 seconds for your answer, and a slow handler turns into a retry and a duplicate message.

Handle failures and duplicates

  • A failed run still delivers, with status failed and an error; artifacts[] lists any media it made.
  • Canceled and skipped runs never deliver, so poll for those if you cancel from your side.
  • Deduplicate on request_id, since each retry repeats it.
  • Register a public HTTPS URL. Localhost, private ranges and plain HTTP are rejected at create.

Where to get the secret

Read the signing secret on the Webhooks tab of the dashboard, or from GET /v1/webhooks/signing-secret with a key that has account:read. Store it as SUME_COM_WEBHOOK_SIGNING_SECRET. Job webhooks and run webhooks share it, so one verifier covers both.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume