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.

5 min readSume
All posts

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.

Response handling for POST /v1/images, from the Image API and jobs docs, read 2026-10-03.
StatusBodyWhat your code does
200data[].url, media_type, usage.costDownload the URLs now
202data.status_url, data.result_url, data.jobPoll until terminal, then read the result
400Error envelopeFix the request; do not retry unchanged
402insufficient_creditsTop up; no generation job started
429rate_limited or queue_fullBack off, honour retry-after
502Remapped public reasonFailed 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

All Developers posts

Written by Sume