Store the Sume job webhook, then answer 204: SQLite insert-or-ignore
Commit the event keyed on job_id before you return 2xx, so a retry or a redeliver is harmless and a crash never loses it. Runnable Python and sqlite3 example.

Insert the event into a table keyed on job_id, commit, and only then return a 2xx. If the insert fails, return 500 so Sume retries. If the row already exists, return 204 and do nothing. Process stored rows in a separate worker. This is the order the Sume docs ask for: store the event durably, then return success.
What the docs say to do
The Webhooks page says: after you store the event durably, return any 2xx. Sume retries network errors and non-2xx responses until it uses all attempts, with up to 10 attempts and a 10-second timeout each. It also says to use job_id as the idempotency key on your side, and that Redeliver does not change the destination URL.
| Your handler | HTTP status | Sume's next step |
|---|---|---|
| Row committed | 204 | Stops retrying this delivery |
| Row already present (retry or redeliver) | 204 | Stops retrying; nothing is processed twice |
| Database write failed | 500 | Retries, up to 10 attempts total |
| Handler slower than 10 s | timeout | Counts as a failed attempt and retries |
The handler
INSERT OR IGNORE on a primary key does the dedupe in one statement. The with db: block commits before the function returns. The signature check happens before this function, on the raw body. The example runs as written and uses no network.
import json
import sqlite3
db = sqlite3.connect("webhooks.db")
db.execute(
"CREATE TABLE IF NOT EXISTS events ("
"job_id TEXT PRIMARY KEY, body TEXT NOT NULL, done INTEGER NOT NULL DEFAULT 0)"
)
def on_delivery(raw_body: bytes) -> int:
"""Call after the signature check. Returns the HTTP status to send."""
event = json.loads(raw_body)
try:
with db: # commit before we answer
db.execute(
"INSERT OR IGNORE INTO events (job_id, body) VALUES (?, ?)",
(event["job_id"], raw_body.decode()),
)
except sqlite3.Error:
return 500 # not stored: let Sume retry
return 204 # stored (or already stored): stop the retries
def work_loop():
rows = db.execute("SELECT job_id, body FROM events WHERE done = 0").fetchall()
for job_id, body in rows:
print("process", job_id, json.loads(body)["event"])
with db:
db.execute("UPDATE events SET done = 1 WHERE job_id = ?", (job_id,))
Why the worker is separate
Doing the slow part inside the handler is how a 12-second handler gets retried and runs twice. With a stored row and a done flag, a crash between store and work leaves done = 0, and the worker picks the row up on restart. A redelivered event finds the row and exits.
Keep the status polls as a backup. Delivery is an optimization, and ten refused attempts leave a failed delivery on a job that has finished.
Why insert-or-ignore
The job id is stable across redeliveries, so a primary key on it turns a duplicate delivery into a no-op. Insert the raw event first, then answer 2xx, and do the slow work from a queue. Sume allows 10 seconds per attempt, so a handler that does real processing inline risks a timeout and another delivery.
A 204 is enough as an acknowledgment. If you return a 4xx or 5xx, Sume treats the attempt as failed and tries again 30 seconds later, up to 10 attempts.
Sources
Related posts
More in Developers
- Stream a Sume video download in Python: .part file, then rename
Download /v1/videos/{id}/content with requests stream=True, write a .part file and rename on success. A lost 30 s clip costs $1.875 to $17.334 to re-buy.
- Sume STT: omit duration_seconds and it reserves 1 minute, not 9
A 9-minute file is $0.09 of Sume STT, but without duration_seconds the reserve is 1 minute, $0.01. Send 540. The field takes 1 to 600 seconds.
- Sume 415 unsupported_media_type: read details.received_content_type
A 415 from the Sume API means the body was not sent as application/json. Which client defaults cause it, how to read the details field, and the one-line fix.
- sume/auto on a retried submit: same job, model still sume/auto
A retry of a sume/auto video with the same Idempotency-Key returns the original job, price and route. A Python check, plus why the family is never disclosed.
Written by Sume