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.

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 state | Meaning | On restart |
|---|---|---|
| intended | Key saved, no job id yet | Resubmit with the same key and body |
| submitted | Job id saved | Poll it |
| completed | Status completed | Download if the file is missing |
| failed | Terminal failure | Decide 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
- Square YouTube Short at 1080x1080: set Timeline output size
YouTube classifies square or vertical videos up to 3 minutes as Shorts. Set Timeline 1.0 output width and height to 1080 for square; 1080x1920 for vertical.
- Startup plan accepts 48 jobs, not 50: send 50 AI video clips in waves
Sume admits concurrency plus queue jobs per plan: 6, 24, 48 or 120. Fifty 30-second clips fit only on Scale at once. See waves per plan and how to retry.
- Stitch 10-second AI clips into a 3-minute Short with Timeline 1.0
YouTube's AI Shorts clips top out at 10 seconds, but a Short can run 3 minutes. Join clips and a voice track in Timeline 1.0 within its 200 slots and 1,800 s.
- Stitch AI clips into one MP4 with Sume Timeline in Python
POST /v1/timeline-1.0/render with audio mode silence joins clips into one MP4 for $0.10 a minute. A short Python script, with the plan call first.
Written by Sume