Sume webhook retries for 4.5 minutes: dedupe on job_id in Python
Sume retries a webhook up to 10 times, 30 s apart. Make the effect happen once with a claim row keyed on job_id, shown in runnable Python with SQLite.

How do I make a Sume webhook handler run its effect only once?
Insert the job_id into a table with a primary key before you do any work, and do the work only when the insert succeeded. Sume delivers each terminal event at least once, so a slow or failing endpoint sees the same event again. The claim row turns "at least once" into "once" for your side effects.
Sume's webhook docs say to use job_id as the idempotency key. A video that finishes after a 30 second generation is a good example: the event is job.completed, and you may get it twice if your first response was late.
What does the retry schedule look like?
A delivery is accepted by any 2xx response. Everything else, including a timeout, counts as a failed attempt. With ten attempts and a fixed gap between them, the automatic retry stretches over nine gaps. At the default 30 seconds that is 270 seconds of waiting, about four and a half minutes, plus up to 10 seconds for each attempt that times out.
After the last refused attempt you have a failed delivery and a job that is still finished. The job result stays on the status route, so a missed webhook never loses the video.
| Setting | Value | What it means for you |
|---|---|---|
| Attempts | Up to 10 | Expect duplicates until you return 2xx |
| Spacing | Fixed, 30 s by default | Not exponential backoff |
| Timeout per attempt | 10 s | Acknowledge first, work later |
| Accepted response | Any 2xx | Return 200 or 204 for a duplicate |
| Dedupe key | job_id | Use it as the primary key |
Claim row in Python with SQLite
The insert is the lock. A second delivery hits the primary key, changes zero rows, and is acknowledged without repeating the work. If your work fails, delete the claim and return a 5xx so the next attempt can run.
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("create table seen (job_id text primary key)")
def claim(job_id: str) -> bool:
cur = db.execute("insert or ignore into seen values (?)", (job_id,))
db.commit()
return cur.rowcount == 1
def handle(event: dict) -> int:
if not claim(event["job_id"]):
return 200 # duplicate delivery: accept, do nothing
try:
print("effect once for", event["job_id"])
except Exception:
db.execute("delete from seen where job_id = ?", (event["job_id"],))
db.commit()
return 500 # let Sume retry in 30 s
return 200
evt = {"event": "job.completed", "job_id": "job_demo_1"}
print(handle(evt), handle(evt))Keep the poll as a safety net
Verify the signature before the claim, and return fast. If the endpoint was down for the whole retry window, call GET /v1/jobs/{id}/status for jobs you still consider open, or ask Sume to resend with POST /v1/jobs/{job_id}/webhook/redeliver. A redelivery carries a fresh signature, does not use up one of the ten automatic attempts, and hits your claim row the same way.
Two details keep the claim honest. First, claim after you verify the signature, not before, so a forged request cannot burn a real job id. Second, store the claim in the same database transaction as the effect when you can. If the effect and the claim live in different stores, a crash between them leaves you choosing between a lost effect and a repeated one, and the rule above (delete the claim, return 5xx) picks the repeat only when your effect is itself safe to run twice.
Sources
Related posts
More in Developers
- Sume webhook rotation: upgrade the verifier before you click Rotate
During a rotation window Sume sends two signatures in one header. A receiver that compares the whole header for equality fails every delivery. Fix it first.
- Sume webhook signature mismatch? Check the secret fingerprint header
Each Sume delivery carries x-sume-webhook-secret-fingerprint. Compare it with the dashboard before debugging code. Python verifier that refuses an empty secret.
- SvelteKit +server.ts endpoint for an AI video webhook: request.text()
A SvelteKit POST handler reads request.text(), verifies Sume's HMAC over timestamp.body with node:crypto and returns 401 for bad or missing signatures.
- Swap the AI image model without a redeploy: JSON config hot reload
Read the Sume image model id from a JSON file that reloads when it changes, so a gpt-image-1 shutdown fix is a one-line edit with no deploy. Python, stdlib.
Written by Sume