Sume /content?index=N: default 0, a 302, and a 404 past the last one

What the Sume content route answers for index 0, an index past the last output, a failed job and a running job, plus a Python loop that saves every output.

5 min readSume
All posts

GET /v1/videos/{id}/content redirects (302) to the video for a completed job. index is a non-negative integer that defaults to 0 and selects the output when a model returns more than one. An index past the last output gets 404 video_content_not_found, with job_id and index in details.

The four answers

The route has one decision per job state, and the order matters: a failed job is checked first so it never looks like a job that is merely slow.

The 409 split is the part to code against. job_not_completed is retryable: poll again. job_failed is final and carries the same public reason that the poll's error string shows, so do not wait on it.

Content route answers, from the Sume API source and docs (read 2026-10-08)
Job state / requestStatus and codeWhat to do
completed, index in range302 to the videofollow the redirect
completed, index too high404 video_content_not_foundstop the loop, you have everything
pending or in_progress409 job_not_completedkeep polling
failed409 job_failedread the reason, do not retry the download

Save every output

Most models return one video, so the usual call is index 0. The loop below does not assume it: it asks for 0, 1, 2 and so on, stops on the 404 code, and fails loudly on anything else. It turns off automatic redirects so that the API key is sent only to Sume's own host, and the file fetch uses the URL from the Location header.

import os
import requests

H = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}

def save_all(job_id: str) -> int:
    i = 0
    while True:
        r = requests.get(
            f"https://api.sume.com/v1/videos/{job_id}/content",
            params={"index": i}, headers=H, allow_redirects=False, timeout=30,
        )
        if r.status_code == 404 and r.json()["error"]["code"] == "video_content_not_found":
            return i
        if r.status_code != 302:
            raise RuntimeError(f"{r.status_code}: {r.text[:200]}")
        clip = requests.get(r.headers["Location"], timeout=300)
        clip.raise_for_status()
        with open(f"{job_id}-{i}.mp4", "wb") as f:
            f.write(clip.content)
        i += 1

Why the order of checks matters

A client that treats every non-200 as a reason to retry will hammer the route for a job that has already failed. The failed check comes first on purpose: once a job is failed, the content route answers 409 job_failed and the message is the public reason, the same text as the poll's error field. Log that text with the request id and stop. A job_not_completed answer, by contrast, is the retryable one: the job is queued or running, so go back to polling polling_url rather than calling the content route in a tight loop.

The status code is a part of the contract, too. The redirect is a 302, not a 200 with a body, so an HTTP client that follows redirects by default will return the video bytes, while one that does not (many do not for a cross-host Location) returns the 302 and the header. The loop above handles the second case explicitly.

Notes

A 404 for an unknown job id is a different error (not_found), so the loop above only treats video_content_not_found as the stop signal. If the first call already answers 404 with that code, the job completed with no output at index 0, which is worth a support ticket with the request id from the error body.

The unsigned_urls array in the poll response lists the same content URLs, one per output, so len(unsigned_urls) tells you the count without probing. Both routes need your key; see unsigned_urls need the API key, and the 4K Omni download for a retry on the 409.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume