Sume webhook beat your database write: handle an unknown job_id
A fast Sume job can send its webhook before your submit handler commits the job id. Park the event in an inbox table and reconcile it, in runnable Python.

What if the webhook arrives before I saved the job id?
Accept it, store it in an inbox keyed by job_id, and let the submit path pick it up once the job row exists. Do not return an error just because the id is unknown: a 4xx or 5xx spends one of the ten automatic attempts, and a 2xx for an event you cannot match yet loses nothing if you saved the body.
The race is real when the submit handler does more after the 202 than write one row. If you create an order, charge a card, and only then commit the transaction that holds request_id, a short generation can finish inside that window. Short clips are the ones most likely to win the race.
Three ways to handle the early event
Pick one and keep it consistent. The inbox is the only option that never depends on timing.
| Option | What Sume sees | Trade-off |
|---|---|---|
| Inbox table, return 204 | Delivered on attempt 1 | You must reconcile after the job row commits |
| Return 503 and wait | Failed attempt, retry after about 30 s | Burns one of 10 attempts per miss |
| Ignore, poll later | Delivered | Result waits for your next poll |
Inbox and reconcile in Python
The handler writes the event to the inbox, and attach runs right after you commit the job row. Both are idempotent, so a duplicate delivery does no harm.
import sqlite3
db = sqlite3.connect(":memory:")
db.executescript("""create table jobs (job_id text primary key, order_id text, state text);
create table inbox (job_id text primary key, body text);""")
def on_webhook(event: dict) -> int:
db.execute("insert or ignore into inbox values (?, ?)", (event["job_id"], str(event)))
db.commit()
return 204
def attach(job_id: str, order_id: str) -> str:
db.execute("insert or ignore into jobs values (?, ?, 'open')", (job_id, order_id))
hit = db.execute("select 1 from inbox where job_id = ?", (job_id,)).fetchone()
if hit:
db.execute("update jobs set state = 'done' where job_id = ?", (job_id,))
db.execute("delete from inbox where job_id = ?", (job_id,))
db.commit()
return db.execute("select state from jobs where job_id = ?", (job_id,)).fetchone()[0]
on_webhook({"event": "job.completed", "job_id": "job_early"})
print(attach("job_early", "order-7"))
print(attach("job_late", "order-8"))Sweep the inbox
Rows left in the inbox for longer than your longest submit transaction belong to jobs your service never created, or to a deploy that lost the row. Check them against GET /v1/jobs/{id}/status with the API key that submitted the job: a key reads only the jobs its own member created, so a job from another workspace answers 404. Delete what does not match.
The same reasoning covers the mirror case. If you rely only on webhooks, a job whose event never arrived stays "open" forever in your table. Run a slow sweep over open rows, read each status, and close the ones that are terminal. The poll and the webhook then write the same final state, and whichever arrives first wins without a conflict because both go through the same idempotent update.
Keep the transaction that stores request_id as short as you can, and send the event handler's acknowledgement before any slow work. Both habits shrink the window in which the race can happen, while the inbox makes the remaining window harmless.
Sources
Related posts
More in Developers
- Webhook for an unknown job_id: park it, then reconcile on insert
A Sume job.completed webhook can name a job_id your database has not stored yet. Return 2xx, park the event in SQLite, and apply it when the insert lands.
- Which AI video model makes a 25-second clip? Duration check in Python
Only some Sume video models accept 25 seconds in one request. See each model's duration range and filter a target length in a few lines of runnable Python.
- Which API: speech, transcript, music, captions or audio track?
Pick the Sume endpoint by the job: TTS for narration, STT for a transcript, Music for a score, captions for burned text, audio detach for a video's sound.
- Sume TTS source errors: which are safe to retry (status table)
Every tts_ error code in Sume's script-source API with its HTTP status, whether it charges, and whether a retry can help. A table for client error handling.
Written by Sume