GPT Image 2.5 image URL expired? Save the file in Node

Sume returns GPT Image 2.5 results as signed URLs, not base64, and its docs give no lifetime. Download each file when the call returns. Node example.

4 min readSume
All posts

A GPT Image 2.5 result from Sume is a URL in data[].url, not inline base64, and the docs call that URL signed. They do not state how long it stays valid, so do not store the link as your record of the image: download the file as soon as the call returns and keep your own copy.

This comes from the Image API docs, read 2026-09-29. It applies to POST /v1/images results. Format runs and job artifacts are a different case, covered in Do Sume video URLs expire.

What does the response give me?

A 200 response carries data, an array with one entry per image. Each entry has a url on media.sume.com and a media_type such as image/png. The file extension follows the output_format you asked for: png, jpeg or webp on the GPT Image 2.5 ids.

From Image API, read 2026-09-29.
FieldMeaning
data[].urlSume-hosted, signed URL of one generated image
data[].media_typeFor example image/png or image/webp
usage.costThe USD amount billed to your wallet
Status 202No image yet: a job envelope with status_url and result_url

How do I save the file in Node?

Fetch the image, check the status, then fetch each URL and write the bytes. The script stops on a 202 because that body is a job envelope, not an image; poll the status_url in that case.

import { writeFile } from "node:fs/promises";

const res = await fetch("https://api.sume.com/v1/images", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUME_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "openai/gpt-image-2.5",
    prompt: "a red panda astronaut floating in space, studio lighting",
    output_format: "png",
  }),
});
if (res.status === 202) throw new Error("Job started: poll status_url");
if (!res.ok) throw new Error(`HTTP ${res.status}`);

const { data } = await res.json();
for (const [i, image] of data.entries()) {
  const file = await fetch(image.url);
  if (!file.ok) throw new Error(`Download failed: ${file.status}`);
  await writeFile(`image-${i}.png`, Buffer.from(await file.arrayBuffer()));
}

What if the call returned 202?

Then the render passed the 30-second wait. Poll GET /v1/jobs/{id}/status, then read GET /v1/jobs/{id}/result for the images. The result uses the standard job result shape, not the image body above, so parse it separately. See GPT Image 2.5 request returned 202.

What if I ask for several images or another format?

Each image in data has its own URL, so the loop above saves them all; billing is per image, and cost_usd × n is what you pay. The example hard-codes .png because it asks for png. If you request webp or jpeg, choose the extension from each entry's media_type instead of assuming one.

The docs do not describe an extra header for downloading a result URL, and they give no way to refresh one, which is one more reason to save the file at once.

Why not just store the URL?

Three reasons, all from how the route is documented.

  • The docs describe the URL as signed and give no lifetime, so a saved link may stop working.
  • Your own storage gives you a file you can re-serve, resize or hand to another tool.
  • Anything you later send back to Sume as an image reference must be a public HTTPS URL, so host your copy where it can be fetched.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume