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.

6 min readSume
All posts

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.

Handler decisions by case (read 2026-10-04)
CaseActionStatus to return
Signature invalidDrop401
Job known, not finalApply and mark final2xx
Job known, already finalIgnore the duplicate2xx
Job unknownPark the raw event2xx

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

All Developers posts

Written by Sume