Sume Idempotency-Key design: one key per order and revision
Build Idempotency-Key from your own order id plus a revision number, so retries return the same Sume job and a changed prompt is a deliberate new key.

Build the Idempotency-Key for a Sume submit from something your system already owns, such as order-8823-hero-v1: the business id, what is being made, and a revision number you increment on purpose. A network retry then reuses the key and gets the original job back instead of paying for a second one. A changed prompt under the same key is refused with 409 idempotency_conflict, which is your cue to bump the revision rather than retry.
Random UUIDs per attempt are the common mistake. They look safe, but a retry after a timeout generates a fresh UUID, so the retry is a new paid job. Hashing the whole payload into the key has the opposite flaw: any edit silently becomes a new job, and the conflict that should stop an accidental change never fires.
What the docs promise
| Situation | Result | Where documented |
|---|---|---|
| Retry after timeout, same key and payload | Returns the original job instead of billing a second one | Jobs and results |
| Same key, different operation or payload | 409 idempotency_conflict | Generation admission |
| queue_full or provider_capacity_exceeded | Retry later with the same key | Errors and rate limits |
| POST without a key | Not safe to retry automatically | Errors and rate limits |
The submit function
The key is a pure function of ids you control, so a crashed worker that restarts can recompute it and resubmit safely. The conflict branch throws a distinct message instead of retrying.
const BASE = process.env.SUME_BASE_URL ?? "https://api.sume.com";
// One key per business intent: "order 8823, promo image, revision 1".
export const keyFor = (orderId, kind, revision = 1) => `order-${orderId}-${kind}-v${revision}`;
export async function submitImage({ orderId, revision, prompt }) {
const res = await fetch(`${BASE}/v1/image-1.0/generate`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SUME_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": keyFor(orderId, "hero", revision),
},
body: JSON.stringify({ prompt, mode: "async" }),
});
const body = await res.json();
if (res.status === 409 && body.error?.code === "idempotency_conflict") {
// Same key, different payload. Do not retry: bump the revision on purpose.
throw new Error(`payload changed for revision ${revision}; use revision ${revision + 1}`);
}
if (!res.ok) throw new Error(`${res.status} ${body.error?.code}`);
return body.data.request_id;
}How to use revisions
The behavior below follows the documented rules: an identical retry returns the original job, an edited payload under the same key is a conflict, and a new revision is a new job. I have not replayed this against a live workspace, so check one real replay before relying on exact status codes.
- Retry on timeouts, 5xx and 429 with the same revision.
- Regenerate because the customer changed the brief: increment the revision.
- Store the returned job id against the key in your database, so the key is the lookup and the id is the receipt.
Limits
Keys are not a substitute for tracking jobs. If you never stored the job id and the key has aged out of whatever window the server keeps, a replay could create a new job; the docs do not publish a retention period, so do not assume one. Also keep keys free of secrets and personal data, since they appear in job records as idempotency_key.
Sources
Related posts
More in Developers
- Image reference URL rejected on Sume: localhost, http, private hosts
Sume's Image API rejects localhost, private-network and non-HTTPS reference URLs before submission. A pre-flight check in Python and what to host instead.
- Sume job response: status_url, result_url, events_url, cancel_url
A Sume submit returns four URLs: status_url to poll, result_url once result_ready is true, events_url for the timeline, cancel_url while cancelable is true.
- Sume job status: queue.state, a null position, worker_heartbeat
Why queue.position is null on a Sume job status, what queue.state and worker_heartbeat report, and what a poller should do when a job sits in the queue.
- Sume job usage_summary: reserved, captured, refunded, final
Read usage_summary on a Sume job: status reserved, captured or refunded, amounts in micros, the final flag, and why dollars are micros divided by 1,000,000.
Written by Sume