Shorts season webhooks: a Python receiver that counts episodes

Receive Sume job webhooks for a season of Shorts renders: verify sume-v1, reject an empty secret, de-dupe by job_id, and know when every episode is terminal.

6 min readSume
All posts

What the receiver has to do

A Shorts season is a batch of independent render jobs, and the question that matters is "are all N episodes terminal yet?" Polling answers it, but a webhook lets you stop polling. Sume sends signed terminal events only: job.completed, job.failed and job.canceled, with no progress or partial deliveries. The receiver below verifies the signature, ignores anything unsigned or stale, records each job_id once, and prints how many episodes have reached a terminal state.

The timing is topical. The October platform roundup reports YouTube's Shorts series, with seasons, episodes and sequential playback, rolling out from 23 September on web, mobile and TV. Publishing a season in order is a batch problem, and a receiver that knows which episode numbers are done is the first piece.

The signature, exactly as Sume documents it

Each delivery carries x-sume-webhook-timestamp and x-sume-webhook-signature. The signed string is <timestamp>.<raw_body>, hashed with HMAC-SHA256 under your signing secret, and the header value looks like sume-v1=<hex>. During a secret rotation the header carries one entry per live secret, newest first, separated by commas, so accept the delivery if any sume-v1= entry matches. Reject the callback when the timestamp is outside your replay window; 300 seconds is the default in Sume's own verifier.

Two details cause most bugs. Verify against the raw bytes, not a re-serialized JSON object, because whitespace changes the HMAC. And refuse an empty secret before comparing anything: an unset environment variable would otherwise turn into a verifier that signs with an empty key. The code below returns false when the secret is empty.

The receiver (Python standard library only)

Set SUME_COM_WEBHOOK_SIGNING_SECRET to the secret from the Webhooks tab of the dashboard. Fill EPISODES with the request_id values returned when you submitted each render, mapped to the episode number you track. The handler answers 200 only after it has recorded the event.

import hashlib, hmac, json, os, time
from http.server import BaseHTTPRequestHandler, HTTPServer
SECRET = os.environ.get("SUME_COM_WEBHOOK_SIGNING_SECRET", "")
EPISODES = {"job_a": 1, "job_b": 2}  # job_id -> episode, saved when you submitted
DONE = {}

def verify(ts, header, raw, secret, tolerance=300):
    try:
        if not secret or abs(time.time() - int(ts)) > tolerance:
            return False
    except (TypeError, ValueError):
        return False
    mac = hmac.new(secret.encode(), 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)))
        h = self.headers
        if not verify(h.get("x-sume-webhook-timestamp"),
                      h.get("x-sume-webhook-signature", ""), raw, SECRET):
            return self.send_response(401), self.end_headers()
        event = json.loads(raw)
        if event.get("job_id") in EPISODES:
            DONE[event["job_id"]] = event["event"]  # same job_id twice is harmless
        print(sorted(EPISODES[j] for j in DONE), "of", len(EPISODES), "episodes terminal")
        self.send_response(200), self.end_headers()
HTTPServer(("", 8080), Hook).serve_forever()

Delivery behavior you can rely on

These are the documented numbers; design the receiver around them.

Webhook delivery facts from the Sume docs (read 2026-10-06)
TopicDocumented behaviorWhat to do in the receiver
Eventsjob.completed, job.failed, job.canceled onlyTreat all three as terminal for the episode
RetriesUp to 10 attempts, fixed spacing (30 s default)Record by job_id so a repeat changes nothing
Timeout10 s per attemptStore the event, then answer; do slow work later
After 10 refusalsDelivery fails, the job still finishedKeep polling status_url as the recovery path
RedeliverFresh timestamp and signature, not counted in the 10Verify the new signature like any other

Failure paths worth testing

Use the dashboard's Send test, or POST /v1/webhooks/test-deliveries with an account:write key, to post a signed webhook.test payload to your URL. That event has no job_id, so the receiver above ignores it and still answers 200, which is what you want from a connectivity check. To replay a real event, call POST /v1/jobs/{job_id}/webhook/redeliver, which re-sends the job's actual terminal event with a fresh signature.

Failed and canceled events use status: "ERROR" and include an error object. For a season that means one episode can end job.failed while the others complete. Do not wait for N completions; wait for N terminal events, then retry only the failed episodes with new idempotency keys. The job's result and the status_url stay available if a delivery never arrives, and the documented loop for that is in jobs and results.

Where this fits in a season pipeline

Submit each Timeline 1.0 render with a webhook_url, keep the returned request_id against the episode number, and let this process fill in DONE. Replace the dictionary with a database table keyed on job_id, since a process restart would otherwise forget what finished. Once every episode is terminal, publish in episode order, not completion order, because renders finish out of order.

Two production habits are worth adding early. Return a 2xx only after the event is stored, because Sume treats anything else as a failed attempt and spends one of its ten. And keep the status poll as a nightly reconciliation: list the episodes you submitted, read each job's status, and compare against what the webhook recorded. A missed delivery then shows up as a gap you can fill, not as an episode that never published.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume