GPT Image 2.5 in TypeScript: fetch that handles 200 and 202
A runnable TypeScript fetch call to Sume's POST /v1/images for GPT Image 2.5, with the 202 job fallback: poll status_url, then read result_url.

To call GPT Image 2.5 from TypeScript, fetch https://api.sume.com/v1/images with model: "openai/gpt-image-2.5", a prompt and a bearer key, then branch on the status code. A 200 carries data[].url; a 202 means generation outlived Sume's 30-second blocking wait and you now hold a job: poll its status_url until terminal is true, then read result_url.
Skipping the 202 branch is the common bug, because the two bodies have different shapes. The code below handles both with Node 18 or newer and no dependencies. Behaviour is from the Image API docs and Jobs and results, read 2026-10-03.
What does the full script look like?
Save it as image.ts and run it with a TypeScript runner such as npx tsx image.ts, with SUME_API_KEY in the environment. It asks for medium quality, since Sume defaults an omitted quality to high. The result reader walks the JSON for image artifacts instead of assuming one exact nesting, so a small envelope change does not break it.
const H = { Authorization: `Bearer ${process.env.SUME_API_KEY}`, "Content-Type": "application/json" };
const urls = (x: any): string[] =>
Array.isArray(x) ? x.flatMap(urls)
: x && typeof x === "object"
? [...(typeof x.url === "string" && String(x.content_type ?? x.media_type ?? "").startsWith("image/") ? [x.url] : []), ...Object.values(x).flatMap(urls)]
: [];
async function main() {
const res = await fetch("https://api.sume.com/v1/images", {
method: "POST", headers: H,
body: JSON.stringify({ model: "openai/gpt-image-2.5", quality: "medium", image_size: "1024x1024",
prompt: "A paper boat on a calm lake at dawn, soft mist" }),
});
const body: any = await res.json();
if (res.status === 200) return console.log([...new Set(urls(body.data))]);
if (res.status !== 202) throw new Error(`${res.status} ${JSON.stringify(body)}`);
const { status_url, result_url } = body.data;
for (;;) {
const raw: any = await (await fetch(status_url, { headers: H })).json();
const s = raw.data ?? raw;
if (s.terminal) {
if (s.sume_status !== "completed") throw new Error(`job ${s.sume_status}`);
break;
}
await new Promise((r) => setTimeout(r, (s.next_poll_after_seconds || 3) * 1000));
}
const out: any = await (await fetch(result_url, { headers: H })).json();
console.log([...new Set(urls(out))]);
}
main();Why does the 202 branch exist?
Sume caps a blocking wait on the images route at 30 seconds by default (wait_timeout_seconds takes 0 to 30). Most catalog models finish inside it. Slow configurations such as 4K output, high quality or a larger n are the ones most likely to degrade to a 202, according to the docs.
A 202 is not a failure and you should not resubmit. Sume's jobs page is explicit: if the job is not terminal, poll, do not resubmit. Resubmitting creates a second paid job.
Which fields should I check on each response?
Branch on these, and nothing else, for a first working client.
| Status | Body | What your code does |
|---|---|---|
| 200 | data[].url, media_type, usage.cost | Download the URLs now |
| 202 | data.status_url, data.result_url, data.job | Poll until terminal, then read the result |
| 400 | Error envelope | Fix the request; do not retry unchanged |
| 402 | insufficient_credits | Top up; no generation job started |
| 429 | rate_limited or queue_full | Back off, honour retry-after |
| 502 | Remapped public reason | Failed inside the budget; not billed |
How do I make retries safe?
Send an Idempotency-Key header on a submit you might retry, and reuse the same key only for the same payload. Sume's error docs say not to retry unsafe submit requests without one. Add it to the headers object per call, for example a key built from your own record id.
Also set a client-side deadline on the polling loop. The docs note that a client-side timeout does not cancel the job; it keeps running and keeps billing, so store the job id and pick it back up from status_url, or cancel it explicitly while it is still cancelable.
What does the script not do?
It does not stream partial images, because Sume returns 400 streaming_not_supported for stream: true. It does not download the files either; the URLs it prints are Sume-hosted, so fetch and store them yourself. For long jobs, a signed webhook is a better fit than a poll loop, and the webhook docs cover the verifier.
How do I run it in a project?
Drop the main function into a route handler or a worker rather than a browser, because the key must stay on the server. In a web app, return the job id to the browser and poll your own endpoint, so the Sume key never reaches client code. Type the response shapes with your own interfaces once you know which fields you read: for the 200 path that is data[].url and usage.cost, and for the job path it is terminal, sume_status and next_poll_after_seconds.
Log the x-sume-request-id response header on errors. Sume's error docs ask you to include the request id when you report an issue, and they ask you not to include API keys or signed URLs in the report.
Sources
Related posts
More in Developers
- H3 Max Recast job failed: what Sume refunds and what to retry
A failed Recast job releases its hold on Sume. Read the error category, fix input errors, retry queue errors with the same Idempotency-Key, never double-submit.
- H3 Max Recast seed: fal has one, Sume does not send it
fal's Recast API takes and returns a seed. Sume's Video Router accepts none, so each run is a new take. How to re-roll, what to vary, and what it costs.
- H3 Max Recast webhook: submit with mode webhook, verify the HMAC
Recast jobs run for a while. Submit h3-max-recast with mode webhook, then verify Sume's sume-v1 HMAC signature before you download the swapped video.
- Ideogram 4 download: Hugging Face gate, login and first image
To run Ideogram 4 locally: accept the gate on Hugging Face, log in with hf, pip install the repo, run run_inference.py. The flags and the nf4 or fp8 choice.
Written by Sume