SQLite ledger: a restarted poller never resubmits a 30-second video

Save the Sume job id and Idempotency-Key in SQLite before you poll, so a crash resumes the same $17 render instead of paying twice. Python, stdlib.

5 min readSume
All posts

Before you poll a long render, write its job id to a local table keyed by your own clip name, and on startup poll the open rows instead of submitting again. A 30-second Seedance 2.5 clip at 720p is $17.34, so a script that forgets its job id on a restart can double the bill for that clip.

SQLite is enough for one machine and needs no server. The pattern works with any store; the rule is to save the id first and the state second.

The two moments that can lose a job

The first is the gap between sending the submit and receiving the response. If the process dies there, you may have a paid job you have no id for. The Idempotency-Key header exists for this: resend the same key with the same body and Sume returns the original job instead of creating a new one; a different body with the same key is 409 idempotency_conflict. So write the key to the ledger before the submit.

The second is the poll itself. A client timeout does not cancel a job, so a crashed poller leaves a job that continues and bills. The ledger row lets the next run pick it up.

Ledger states for a 30-second render, Sume job docs read 2026-10-05
Ledger stateMeaningOn restart
intendedKey saved, no job id yetResubmit with the same key and body
submittedJob id savedPoll it
completedStatus completedDownload if the file is missing
failedTerminal failureDecide whether to retry

The code

The script handles one clip: it looks up the row by name, resubmits with the stored key if needed, and polls to a terminal state. The submit body here mirrors the documented /v1/videos fields.

import json, os, sqlite3, time, uuid, urllib.request

KEY = os.environ["SUME_API_KEY"]
db = sqlite3.connect("ledger.db")
db.execute("create table if not exists t(name text primary key, idem text, job text, state text)")

def call(method, url, body=None, idem=None):
    h = {"Authorization": "Bearer " + KEY, "Content-Type": "application/json"}
    if idem: h["Idempotency-Key"] = idem
    data = json.dumps(body).encode() if body else None
    with urllib.request.urlopen(urllib.request.Request(url, data, h, method=method)) as r:
        return json.load(r)

name, body = "intro-30s", {"model": "seedance-2.5", "duration": 30,
    "resolution": "720p", "prompt": "A lighthouse in a storm, slow push-in"}
row = db.execute("select idem, job from t where name=?", (name,)).fetchone()
if not row:
    row = (str(uuid.uuid4()), None)
    db.execute("insert into t values(?,?,?,?)", (name, row[0], None, "intended")); db.commit()
idem, job = row
if not job:
    job = call("POST", "https://api.sume.com/v1/videos", body, idem)["id"]
    db.execute("update t set job=?, state='submitted' where name=?", (job, name)); db.commit()
while (s := call("GET", f"https://api.sume.com/v1/videos/{job}"))["status"] in ("pending", "in_progress"):
    time.sleep(20)
db.execute("update t set state=? where name=?", (s["status"], name)); db.commit()
print(name, job, s["status"])

Notes

  • Keep the body identical on a resubmit. Build it from the ledger row or a fixed config, not from a prompt that changes.
  • The key lives as long as the ledger row, not as long as the process.
  • If you also use webhooks, the same table is where you dedupe on job_id.
  • Delete rows by hand after you have downloaded the file; the ledger is a record, not a queue.

Scaling it past one clip

For a batch, loop over the rows. Insert all clips in the intended state first, then submit in waves that respect your plan's queue, and let a single poller update the open rows. Because each row has its own key, a restart resubmits only those still intended.

If you run on several machines, move the ledger to your own database and add a unique constraint on the clip name. SQLite is a good first step, not a fleet answer.

One more check is worth adding: before a resubmit, read the account balance, because the estimate is reserved at submit and an empty balance returns a 402 before any work starts.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume