Node 22.23.3 fetch: retry a Sume submit with one Idempotency-Key
Node 22.23.3 LTS bundles Undici 6.28.1. A fetch submit loop that retries 429 and 5xx with one Idempotency-Key so a retry never bills twice.

Short answer
On Node 22.23.3 the built-in fetch is enough: build the Idempotency-Key once per intent, outside the retry loop, then retry only 429 and 5xx with a pause from retry-after. Node's 22.23.3 release notes list Undici 6.28.1 among the updated dependencies and give the release date as 23 September 2026.
The key placement is the whole point. Sume's jobs guide says a retry with the same key returns the original job instead of billing a second one, and its errors page says not to retry unsafe submits without a key. A key generated inside the loop is a new intent on every attempt.
What the 22.23.3 notes say
The notes are a maintenance release. The facts that touch an HTTP client are the Undici bump and the certificate bundle, both of which are inherited by fetch without code changes.
| Area | Change |
|---|---|
| Undici | updated to 6.28.1 |
| Root certificates | updated to NSS 3.125 |
| OpenSSL | updated to 3.5.8 |
| npm and Corepack | npm 10.9.9, Corepack 0.36.0 |
Which statuses are safe to retry
Not every failure is worth another attempt. A 400 or 402 will fail the same way again, so the loop below returns those bodies to the caller. A 409 idempotency_conflict means the key was reused with a different payload, which is a bug in how the key is derived.
| Status | Code | Loop behavior |
|---|---|---|
| 202 | job envelope | return it and start polling |
| 429 | rate_limited, queue_full | wait retry-after (or backoff) and retry with the same key |
| 503 | provider_capacity_exceeded | wait and retry with the same key |
| 400, 402, 409 | invalid_request, insufficient_credits, idempotency_conflict | return to the caller; a retry cannot fix it |
The submit helper
Save as submit.mjs so top-level await works. AbortSignal.timeout bounds each attempt; the job itself is not cancelled if an attempt times out, which is why the retry reuses the key and gets the original job back.
const url = "https://api.sume.com/v1/image-1.0/generate";
const key = `hero-${new Date().toISOString().slice(0, 10)}-001`;
async function submit(prompt, attempts = 4) {
for (let i = 0; i < attempts; i++) {
try {
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SUME_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": key,
},
body: JSON.stringify({ prompt, mode: "async" }),
signal: AbortSignal.timeout(20_000),
});
if (res.status !== 429 && res.status < 500) return await res.json();
const wait = Number(res.headers.get("retry-after")) || 2 ** i;
await new Promise((r) => setTimeout(r, wait * 1000));
} catch (err) {
if (i === attempts - 1) throw err;
await new Promise((r) => setTimeout(r, 2 ** i * 1000));
}
}
throw new Error("Sume submit still failing; poll GET /v1/jobs with the key's job id");
}
console.log(await submit("Product hero shot of a matte black bottle on marble"));What Sume does and does not do
Sume answers a replayed key with the original job, and answers a changed payload under the same key with 409 idempotency_conflict. It reports which budget a 429 spent in error.details.scope (read or write), and it sends retry-after on 429.
Sume does not retry for you, and it does not infer intent from the prompt. Derive the key from something stable in your own system, such as a content id plus a version, so a crashed worker that restarts produces the same key.
Sources
Related posts
More in Developers
- Node-RED http request: node headers overwrite msg.headers (Sume key)
Node-RED's http request node lets node-configured headers overwrite msg.headers. Keep Sume's one credential header in the node, per-call headers in the msg.
- Node-RED http request node: submit a Sume job and branch on statusCode
Node-RED's http request node returns payload, statusCode and headers. Wire a Sume submit, branch on 202 vs errors, and keep the job id for the status read.
- og:image width, height and alt tags for an AI image from Sume
Open Graph has optional og:image:width, og:image:height and og:image:alt. Read the real size from a Sume render with Pillow and print all three tags.
- One reference image on Seedance 2.5 is not a first frame
On /v1/videos, input_references steer a generation; frame_images pin frames. Send one image the wrong way and the clip does not start on your picture.
Written by Sume