Sora download_content vs Sume /content: the 302 and two 409 errors
OpenAI's content endpoint took variant=video, thumbnail or spritesheet. Sume's /content redirects with a 302 and returns two different 409s. How to branch.

On Sume, GET /v1/videos/{id}/content does not stream bytes: it answers 302 with a Location pointing at the artifact, accepts ?index=0 to pick one output, and needs your API key on the request to /content itself. If the job is not done it answers 409 job_not_completed, which you retry; if the job died it answers 409 job_failed, which you do not.
That replaces OpenAI's download_content(video_id, variant=...), which the guide (read 2026-10-08) describes with three variants: video, thumbnail as webp, and spritesheet as jpg, with download URLs valid for at most one hour.
Side by side
The practical difference is that Sume has one output per index, not three variants of one asset. Stills and sprite sheets are a separate job, covered in the thumbnail and spritesheet replacement post.
| Question | OpenAI guide | Sume docs |
|---|---|---|
| Which file | variant: video, thumbnail, spritesheet | index: 0 by default |
| Response shape | bytes through the SDK helper | 302 redirect to the artifact URL |
| Not ready yet | poll status until completed | 409 job_not_completed, retryable |
| Job failed | status failed | 409 job_failed, not retryable |
| Auth on the content call | API key | API key (Bearer or x-api-key, one of them) |
A fetch that branches correctly
With Node 18 or later, set redirect to manual so you see the 302 and can decide what to do with the Location. Run it as an ES module with SUME_API_KEY set and a real job id in JOB_ID.
const base = "https://api.sume.com";
const headers = { authorization: `Bearer ${process.env.SUME_API_KEY}` };
async function artifactUrl(jobId) {
const res = await fetch(`${base}/v1/videos/${jobId}/content?index=0`, {
headers,
redirect: "manual",
});
if (res.status === 302) return res.headers.get("location");
if (res.status === 409) {
const body = await res.json();
const code = body?.error?.code;
if (code === "job_not_completed") return null;
throw new Error(`job failed: ${code}`);
}
throw new Error(`unexpected ${res.status}`);
}
const url = await artifactUrl(process.env.JOB_ID);
console.log(url ?? "not ready, poll again");Notes on the error body
The snippet reads error.code, the field the API's OpenAPI text names for both 409 answers, and error.message carries the same public reason the poll response reports. A job_failed answer also carries retryable false. In Node, manual redirect mode returns the 302 response with its headers readable, unlike a browser, where the response is opaque.
Do not hand the Location URL to your users as a permanent link. Copy the file to your own storage after the first successful fetch, as the post on one-hour URLs argues.
Sources
Related posts
More in Developers
- Sora input_reference image: upload with uploadFile, use frame_images
OpenAI took the first-frame image as multipart input_reference. Sume wants an HTTPS URL: upload with the SDK's uploadFile, then pass it as frame_images.
- Sora jobs in flight at shutdown: reconcile, then resubmit to Sume
OpenAI's page gives the removal date but not in-flight jobs. Mark every unfinished Sora row, then resubmit its prompt to Sume with a key built from the old id.
- Measure your own render time on Sume from job event timestamps
No published Sora timing carries over. Read GET /v1/jobs/{id}/events, diff created, started and completed, and keep your own queue and render split per model.
- Sora to Sume pull request review: eight lines to check before merge
A reviewer's checklist for a Sora-to-Sume PR: duration and resolution types, status words, the 302 download, the webhook body, idempotency and removed fields.
Written by Sume