Resume polling Sume video jobs after a restart with a sqlite ledger
Write the job id and Idempotency-Key to sqlite before anything else can crash, then resume polling open jobs on start. Python standard library only.

A video job outlives your process. If your worker restarts mid-wait, the job keeps running on Sume's side, and GET /v1/videos/{id} still answers. The only thing you can lose is the job id, so write it to disk first. The Python script below keeps a sqlite table of job ids with status and cost, and on start it polls every job that is not completed, failed or cancelled until all are closed.
Where a crash loses data
The risky gap is between sending the POST and storing the id. If the process dies there, Sume has a job you cannot name. The Idempotency-Key closes the gap: store the key before the POST, and after a restart send the same submit again; Sume returns the original job instead of creating a second one. The table sets out each crash point.
| Crash happens | What is lost | Recovery |
|---|---|---|
| Before the POST | Nothing | Submit again |
| After the POST, before the id is stored | The job id | Repeat the POST with the stored Idempotency-Key and get the original job |
| After the id is stored, during polling | Nothing | resume() polls the open ids |
| After completion, before download | The local file | Fetch the content route again |
The ledger
The jobs table has three columns: id, status and cost. The script inserts a row with status pending using insert or ignore, so running it twice is harmless. resume() selects every row whose status is not terminal, polls it, writes the new status and usage.cost, and sleeps between rounds. It returns when the open list is empty.
The sample inserts a placeholder id (job_1) to show the shape. In real code, insert the id you receive from the 202, and store the Idempotency-Key in its own column before you submit.
import json, os, sqlite3, time, urllib.request
BASE = "https://api.sume.com"
db = sqlite3.connect("jobs.db")
db.execute("create table if not exists jobs (id text primary key, status text, cost real)")
def get(path):
req = urllib.request.Request(BASE + path, headers={"Authorization": "Bearer " + os.environ["SUME_API_KEY"]})
with urllib.request.urlopen(req) as r:
return json.load(r)
def resume(delay=2.0):
while True:
open_ids = [r[0] for r in db.execute("select id from jobs where status not in ('completed','failed','cancelled')")]
if not open_ids:
return
for job_id in open_ids:
s = get(f"/v1/videos/{job_id}")
db.execute("update jobs set status=?, cost=? where id=?", (s["status"], (s.get("usage") or {}).get("cost"), job_id))
db.commit()
print(job_id, s["status"])
time.sleep(delay)
# at submit time, write the id before anything else can crash:
db.execute("insert or ignore into jobs (id, status) values (?, 'pending')", ("job_1",))
db.commit()
resume()Adjust before production
Add a deadline per job, and a counter for transient read errors so one 5xx does not end the loop. Use the video route and the status vocabulary of /v1/videos: pending, in_progress, completed, failed, cancelled. If you also read /v1/jobs/{id}/status, note that its vocabulary is queued, processing, completed, failed and canceled, with one l in canceled.
For many jobs, poll with a spread of intervals instead of one round every 2 seconds; the read budget is generous, but the logs are not.
A good test is to kill the process on purpose. Submit two cheap jobs, stop the script after the first row is written, start it again, and check that both rows reach a terminal status without a second submit. If a row stays pending after the job completed, the update statement or the commit is missing. Keep the database file out of version control. Also record the model and the requested duration in the ledger, so the cost column can later be checked against the price list for that clip.
- Commit after every row update.
- Keep the ledger file on durable storage.
- Compare ledger totals against usage in the dashboard.
Sources
Related posts
More in Developers
- Scheduled run input: 64 properties and 2 MiB, how to pack a brief
A Sume scheduled run takes an input object of at most 64 properties and 2,097,152 bytes. Here is how to pack a brief for an agent without hitting either limit.
- Seedance 2.5 in 16:9, 9:16 and 1:1: one idempotency key per ratio
Fan one prompt out to three aspect ratios on /v1/videos, derive a separate Idempotency-Key for each ratio, and rerun the script without paying twice.
- Send a signed webhook.test with POST /v1/webhooks/test-deliveries
Point POST /v1/webhooks/test-deliveries at a new HTTPS endpoint to get a signed webhook.test before any video job runs, then check the sume-v1 signature.
- Seven pre-flight checks before an Omni, H3 or Recast job
Duration, resolution, ratio, edit conflicts, reference counts, refused fields and URLs: seven per-model checks that catch a refused Sume video request.
Written by Sume