Webhook and poll race on a Sume video: one SQLite insert decides
Keep polling as a backup to the job webhook. Dedupe on job_id with INSERT OR IGNORE so only one path downloads. 10 deliveries 30 s apart cover 270 s.

Sume retries a job webhook up to 10 times, 30 s apart, so the last attempt can land 270 s after the first, and your poller may have finished the same job minutes earlier. Insert the job_id into a table with INSERT OR IGNORE, and download only when the insert actually added a row.
The delivery window
Deliveries have a 10 s timeout each, and the docs say to dedupe on job_id and keep polling as a backup. Webhook events are terminal only: job.completed, job.failed and job.canceled.
Attempt n lands about (n - 1) x 30 s after the first, if every earlier attempt failed.
| Attempt | Earliest time after first | Cumulative wait |
|---|---|---|
| 1 | 0 s | 0 s |
| 2 | 30 s | 30 s |
| 5 | 120 s | 2 min |
| 10 | 270 s | 4.5 min |
The guard and the signature check
Both paths call first_time(job_id) before touching storage. The verifier refuses an empty secret, rejects stale timestamps beyond 300 s and accepts any of the comma-separated signatures sent during a secret rotation.
import hashlib, hmac, os, sqlite3, time
SECRET = os.environ.get("SUME_COM_WEBHOOK_SIGNING_SECRET", "")
if not SECRET:
raise SystemExit("signing secret missing")
def verify(raw: bytes, ts: str, header: str) -> bool:
if abs(time.time() - int(ts)) > 300:
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(","))
db = sqlite3.connect("seen.db")
db.execute("create table if not exists seen(job_id text primary key)")
def first_time(job_id: str) -> bool:
cur = db.execute("insert or ignore into seen values (?)", (job_id,))
db.commit()
return cur.rowcount == 1Gotchas
Verify the raw body bytes, before any JSON parsing; the signed string is the timestamp, a dot and the exact body. Return 2xx quickly and do the download after, because the delivery times out at 10 s.
The header is x-sume-webhook-signature with the prefix sume-v1=, next to x-sume-webhook-timestamp (Unix seconds). For a 30 s Wan clip at 720p ($3.75), a duplicate download wastes bandwidth, not money; for a duplicate downstream post it can cost more, so dedupe anyway.
Sources
Related posts
More in Developers
- Sume webhook delivery statuses: pending to exhausted, and your move
A Sume job webhook has six delivery states, from pending to exhausted. What each means for your receiver, where to read it, and when to redeliver or poll.
- A Sume webhook receiver in a Cloudflare Worker with verifyWebhook
verifyWebhook uses WebCrypto, not node:crypto, so it can run in a Worker. A fetch handler that refuses an empty secret, checks the signature, returns 204.
- Sume webhook retries: 10 attempts, 270 seconds of gaps, then poll
How long does Sume keep retrying a job webhook? Ten attempts, a 30 second default gap and a 10 second timeout, worked out in seconds, plus a Python dedupe.
- Route Sume job webhooks: handle job.canceled, answer 204 to the rest
Sume sends job.completed, job.failed and job.canceled; run webhooks use another name. A TypeScript router that handles each and returns 204 for the rest.
Written by Sume