Store the Idempotency-Key before you submit: a SQLite intent table

Write key and body to SQLite, submit with that key, then store the job id. A crash between steps replays the same key and returns the original Sume job.

4 min readSume
All posts

To make a Sume submit crash-safe, write the intent and its Idempotency-Key to durable storage before the HTTP call, send the request with that key, and store the job id from the response. If the process dies between any two steps, the next run finds the row and reuses the key, so the retry returns the original job instead of creating and billing a second one.

The Sume docs say to send Idempotency-Key on submit requests when retrying after client-side timeouts or network failures, and to reuse it only for the same operation and payload. A table keyed by that value is the smallest piece of state that makes both rules automatic.

The three crash points

There are three places a process can die. Each one needs a different recovery, and the intent table covers them all (the endpoint and fields are from the Sume jobs docs, read 2026-10-03).

Where a submit can fail and what the intent row does (read 2026-10-03)
Crash pointRow state on restartRecovery
Before the row is writtenNo rowNothing was sent; start normally
After the row, before the responsekey set, job_id emptyResend with the same key; Sume returns the original job if it was accepted
After the response, before the updatekey set, job_id emptySame as above; the replay returns the job id again
After the updatejob_id setSkip the submit; poll the job

The code

It uses only the standard library. The body is serialized with sorted keys so the same intent always produces the same text, which doubles as the lookup column. The response field request_id is the job id, as documented for the generate endpoints. Set SUME_API_KEY before running.

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

db = sqlite3.connect("intents.db")
db.execute("create table if not exists intent (key text primary key, body text unique, job_id text)")

def submit(body: dict) -> str:
    payload = json.dumps(body, sort_keys=True)
    row = db.execute("select key, job_id from intent where body = ?", (payload,)).fetchone()
    if row and row[1]:
        return row[1]
    key = row[0] if row else str(uuid.uuid4())
    if not row:
        db.execute("insert into intent (key, body) values (?, ?)", (key, payload))
        db.commit()
    req = urllib.request.Request(
        "https://api.sume.com/v1/image-1.0/generate",
        data=payload.encode(),
        headers={"Authorization": "Bearer " + os.environ["SUME_API_KEY"],
                 "Content-Type": "application/json", "Idempotency-Key": key},
    )
    with urllib.request.urlopen(req) as resp:
        job_id = json.load(resp)["request_id"]
    db.execute("update intent set job_id = ? where key = ?", (job_id, key))
    db.commit()
    return job_id

print(submit({"prompt": "Product hero shot of a matte black bottle", "mode": "async"}))

Limits of this pattern

Two identical bodies are treated as one intent, so a second request for the same image needs a different body, for example a seed or a prompt change. If you want deliberate duplicates, add your own business key to the table instead of keying on the body.

The key never changes for an intent, which also means you must not edit the stored body after the first send, because the same key with a different payload is a 409 idempotency_conflict. Once job_id is set, switch to polling, and use list and recover for rows that were stored without an id. Keys longer than your own business key, up to 255 characters, are covered in hashing long business keys.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume