Retry a failed Format create with the same Idempotency-Key
After a 402 or 503 on a Format create, Sume releases the Idempotency-Key. Fix the cause, then retry with the same key instead of minting a new one.

Yes: reuse it. When a Format create fails with 402 or 503, Sume releases the Idempotency-Key, so the same key works again once you have fixed the cause. Keep the key stable across all your attempts for one order. If you generate a new key per attempt, an attempt that did land (for example, one whose response you never saw) can start a second run next to the first.
Which failures free the key
The create call can fail before a run exists. Nothing ran in these cases, and nothing was charged: a 4xx at create costs nothing. What to do depends on the code.
| Status | `error.code` | Do this, with the same key |
|---|---|---|
| 402 | insufficient_credits | Fund the workspace (next_action: add_funds), then send again |
| 402 | organization_wallet_not_provisioned | An admin must fund the organization wallet, then send again |
| 503 | studio_agent_upstream_unavailable | A Sume-side outage. Retry the same request later |
| 409 | idempotency_key_in_use | A duplicate is in flight. Wait about one second, then send again to get the original run |
| 409 | idempotency_conflict | Do not retry. The body differs from the first request that used this key |
The two 409 codes behave differently. idempotency_key_in_use is marked retryable: true, and the retry returns the original run once the first request has finished. idempotency_conflict is a bug in your key derivation or your body, so repeating it only repeats the error.
A retry helper
The sample sends the create with a caller-supplied key, retries only the retryable cases after a one-second wait, and stops on a 402 with a message that tells the operator what to do. It reads SUME_API_KEY and optionally SUME_API_BASE_URL from the environment. It accepts the receipt either wrapped in data or bare.
const key = process.env.SUME_API_KEY;
const base = process.env.SUME_API_BASE_URL ?? "https://api.sume.com/v1";
if (!key) throw new Error("set SUME_API_KEY");
// A create that failed (402, 503) released the Idempotency-Key. Reuse it after the fix.
export async function createWithKey(path, body, idemKey, attempts = 3) {
for (let i = 1; i <= attempts; i++) {
const res = await fetch(`${base}${path}`, {
method: "POST",
headers: { "x-api-key": key, "content-type": "application/json", "idempotency-key": idemKey },
body: JSON.stringify(body),
});
if (res.status === 409 || res.status === 503) {
const err = (await res.json()).error;
// idempotency_key_in_use is retryable: wait about one second, send the same request
if (err?.code === "idempotency_key_in_use" || res.status === 503) {
await new Promise((r) => setTimeout(r, 1000));
continue;
}
throw new Error(err?.code);
}
if (res.status === 402) throw new Error("insufficient_credits: add funds, then call again with the same key");
return res.json();
}
throw new Error("still failing after retries");
}
const run = await createWithKey("/formats/acme/product-promo/runs", { input: {} }, "order-8823-v1");
console.log(run.data?.id ?? run.id);Notes
- Do not retry
402in a loop. Funding is a human or billing action, so surface it and let the order wait. - Cap your attempts. Three is plenty for a
503; after that, alert and keep the order in a pending state. - A
429 rate_limitedon a create is also retryable with the same key. Wait forretry-afterseconds; a create spends the write budget. - A
200withidempotency_hit: truemeans an earlier attempt landed. Store that run id and do not create anything else. - The key's scope is one Format, so keep the key and the Format paired in your records.
Order of operations in a worker
Derive the key before the first attempt and store it on the order. On each attempt, send the same key and the same body. If any attempt returns a receipt, persist data.id and stop. If you run out of attempts, leave the order pending with the last error.code, and let a later job send the identical request again. Changing the body between attempts, even by adding a timestamp to the instruction, turns a harmless retry into 409 idempotency_conflict.
The case this protects
The dangerous failure is the ambiguous one: your client times out, or the connection drops after the request left. You cannot know whether the run exists. With a stable key, the retry either creates the run or returns the original with idempotency_hit: true. Without one, the retry may create a second run, and each run is billed. The SDK's createSumeClient follows the same rule: it retries a POST only when an Idempotency-Key is present. Read the replay table in Calling a Format, and the full error list in Errors and spend.
Sources
Related posts
More in Developers
- Rotate the Sume webhook signing secret without dropping a delivery
Upgrade the verifier first, rotate with POST /v1/webhooks/signing-secret/rotate, deploy the new secret inside the 24-hour two-signature window, then confirm it.
- Route Sume run webhooks by event: format, action and agent terminal
Run webhooks use one terminal event per family: action.run.terminal, format.run.terminal, agent.run.terminal. Route on event, then branch on outcome.
- Ruby Net::HTTP: create a Sume bulk queue and poll to an exit code
A 30-line Ruby script with only the standard library: create a Sume bulk queue from items.json, back off the poll, skip 429 and 503, and exit 1 on failed items.
- SHA-256 the batch body into the Sume idempotency key for bulk chunks
Derive each bulk chunk's Idempotency-Key from a hash of its items so replays reuse the queue and edited rows get a new key instead of a 409 conflict.
Written by Sume