Node fetch with AbortSignal.timeout: poll a Sume job
A Node recipe: submit a Sume job with fetch, bound each call with AbortSignal.timeout, poll status_url, read the result. A local timeout does not cancel it.

Bound every fetch to a Sume job with AbortSignal.timeout(ms), submit with mode: "async" and an Idempotency-Key, then poll status_url until terminal is true. The timeout only aborts your local request: Sume's docs say a client-side timeout does not cancel the job, which keeps running and billing, so store the job id and resume from status_url instead of resubmitting.
The signal behavior is from MDN's AbortSignal.timeout() page; the job contract is from Jobs and results and the API reference. All read 2026-10-02.
What does the Node recipe look like?
Save it as poll.mjs so top-level await works, set SUME_API_KEY, and run node poll.mjs. It uses only the global fetch, so there is no dependency to install. Sume's own TypeScript client is covered in the SDK quickstart.
const BASE = "https://api.sume.com";
const headers = { Authorization: `Bearer ${process.env.SUME_API_KEY}` };
const sleep = (s) => new Promise((r) => setTimeout(r, s * 1000));
const call = (url, init = {}) =>
fetch(url, { ...init, headers: { ...headers, ...init.headers }, signal: AbortSignal.timeout(30_000) });
async function generate(prompt, key) {
const r = await call(`${BASE}/v1/images`, {
method: "POST",
headers: { "Content-Type": "application/json", "Idempotency-Key": key },
body: JSON.stringify({ model: "sume/auto", prompt, mode: "async" }),
});
if (!r.ok) throw new Error(`submit ${r.status}: ${await r.text()}`);
const { data: job } = await r.json();
let delay = job.next_poll_after_seconds ?? 2;
for (let i = 0; i < 200; i++) {
await sleep(delay);
const s = await call(job.status_url);
if (s.status === 429) { delay = Number(s.headers.get("retry-after") ?? delay * 2); continue; }
if (!s.ok) throw new Error(`status ${s.status}`);
const { data: st } = await s.json();
if (st.terminal) {
if (st.sume_status !== "completed") throw new Error(await (await call(`${BASE}/v1/jobs/${job.request_id}`)).text());
const res = await (await call(job.result_url)).json();
return res.data.result.artifacts.map((a) => a.url);
}
delay = st.next_poll_after_seconds ?? Math.min(delay * 2, 30);
}
throw new Error(`still running: ${job.status_url}`);
}
console.log(await generate("a red panda astronaut", "panda-order-8823-v1"));What does AbortSignal.timeout do and not do?
MDN says AbortSignal.timeout() returns a signal that aborts after the given number of milliseconds, with a TimeoutError as its reason. The time is active time, not elapsed time, and the timeout cannot be cancelled once created.
| Behavior | Source says | For a Sume job |
|---|---|---|
Aborts after time ms | Reason is a TimeoutError DOMException. | Catch it in your own wrapper and poll again; it is not a job outcome. |
| One signal per call | The timeout cannot be cancelled early. | Make a fresh signal per request, as the call helper does. |
| Local only | It aborts your request, nothing else. | The job keeps running and billing; cancel only through cancel_url. |
What should happen on a timeout or a 429?
A timeout on the submit is the case the idempotency key exists for: retry the same request with the same key and you get the original job back. A timeout on a status call does not affect the job, but the recipe does not catch the TimeoutError: wrap generate in your own retry and resume from the stored status_url. A 429 on a status call means the read budget is spent, so the recipe sleeps for the retry-after seconds and continues. The loop is bounded at 200 polls, which is your own deadline and not an API limit.
What would I add for production?
Four additions turn the recipe into a service component.
- Persist
request_idandstatus_urlbefore the first poll. - Derive the idempotency key from your business intent, not a random value per attempt.
- On a failed job, read
GET /v1/jobs/{id}for the public error:/resultanswers409 job_not_completedfor jobs that did not complete. - Prefer a signed webhook for long video jobs, with this loop as the backup.
Sources
Related posts
More in Developers
- Open Graph image 1200x630 with an AI API: generate 16:9, then crop
Meta recommends og:image at 1200 x 630 and a 1.91:1 ratio. Sume has no 1.91:1 option, so generate 16:9 and crop with 12 lines of Pillow. Steps and limits.
- OpenAI Agents 0.23 MCP listing page limits and Sume tools_list
Agents SDK 0.23.0 adds configurable MCP listing page limits. For a Sume server: call tools_list once, then tools_schema for the one tool you need.
- OpenAI Agents 0.23 nested agent tools: one Sume key per item
Agents SDK 0.23.0 fixed tool state isolation for nested agents. For paid Sume calls, the idempotency key must still come from your item, not from the agent run.
- OpenAI structured outputs: 5000 properties, 10 levels, vs Sume
OpenAI caps strict schemas at 5000 properties and 10 nesting levels. Sume's output_schema uses the same numbers but counts enums and strings differently.
Written by Sume