A Sume webhook arrives before your database knows the job: park it
If job.completed lands before your submit handler stored the job id, keep the verified event in a parked table, answer 2xx, and reconcile when the id is saved.

Store the verified event in a parked table keyed by job_id, return 2xx, and in the code that saves the job id from the submit response, check parked and apply anything waiting. This is a defensive design of your own, not documented Sume behavior: Sume only tells you that deliveries are retried and that job_id is the dedupe handle.
Why this can happen
Two of your own steps can race. Your submit call returns the job id, you write it to a table, and a fast job can deliver its webhook between the response and the write. The webhook handler then looks up a job it has never seen.
| Case | Action | Status to return |
|---|---|---|
| Signature invalid | Drop | 401 |
| Job known, not final | Apply and mark final | 2xx |
| Job known, already final | Ignore the duplicate | 2xx |
| Job unknown | Park the raw event | 2xx |
Park and reconcile
The script is self-contained: it uses an in-memory SQLite database and runs three scenarios (duplicate, early arrival, normal) so you can see each branch. Signature checking is assumed to have happened before handle_event.
import asyncio, json, sqlite3
def init():
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE jobs (job_id TEXT PRIMARY KEY, state TEXT NOT NULL)")
db.execute("CREATE TABLE parked (job_id TEXT PRIMARY KEY, body TEXT NOT NULL)")
return db
def apply(db, event):
cur = db.execute("UPDATE jobs SET state=? WHERE job_id=? AND state='open'",
(event["event"], event["job_id"]))
return cur.rowcount
def handle_event(db, event):
if db.execute("SELECT 1 FROM jobs WHERE job_id=?", (event["job_id"],)).fetchone() is None:
db.execute("INSERT OR IGNORE INTO parked VALUES (?, ?)", (event["job_id"], json.dumps(event)))
return "parked"
return "applied" if apply(db, event) else "duplicate"
def on_submitted(db, job_id):
db.execute("INSERT OR IGNORE INTO jobs VALUES (?, 'open')", (job_id,))
row = db.execute("SELECT body FROM parked WHERE job_id=?", (job_id,)).fetchone()
if row:
apply(db, json.loads(row[0]))
db.execute("DELETE FROM parked WHERE job_id=?", (job_id,))
return "reconciled"
return "stored"
async def main():
db = init()
done = {"event": "job.completed", "job_id": "job_early"}
print(handle_event(db, done)) # parked
print(on_submitted(db, "job_early")) # reconciled
print(handle_event(db, done)) # duplicate
asyncio.run(main())Clean-up
Parking costs one row per early event. Add a sweep that checks GET /v1/jobs/{id}/status for parked ids older than a few minutes, since a parked event whose job id never arrives may belong to a submit that your process lost.
Backstop
Polling is the backup path in any case: the docs call a webhook a delivery optimization, so a dropped event is recoverable from the status route.
Sources
Related posts
More in Developers
- What a media MCP server should declare at server/discover
MCP 2026-07-28 adds a required server/discover call. A media server has more to say than versions: async jobs, wait limits, scopes. Where Sume documents each.
- Which Lyria ran? job.model vs job.request.routed_model on Sume
On Sume's Music Router, job.model echoes what you sent and job.request.routed_model names the engine that ran. How to read both and pin a Lyria id.
- Which short-video lengths fit one video-trim call? 0.2 to 900 seconds
One Sume video-trim call outputs 0.2 to 900 seconds, so a 3-minute Short fits easily. A 10-minute TikTok ad fits; a 60-minute source does not.
- whisper-1 or gpt-transcribe for subtitles: what OpenAI assigns to each
OpenAI recommends gpt-transcribe, gpt-4o-transcribe-diarize for speakers, whisper-1 for translation and subtitles. Plus the 25 MB limit and a chunking script.
Written by Sume