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.

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.
| Job state / request | Status and code | What to do |
|---|---|---|
| completed, index in range | 302 to the video | follow the redirect |
| completed, index too high | 404 video_content_not_found | stop the loop, you have everything |
| pending or in_progress | 409 job_not_completed | keep polling |
| failed | 409 job_failed | read 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 += 1Why 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
- waitForJob times out at 20 minutes: why it does not fit a Vercel route
Sume SDK waitForJob waits 20 minutes by default and the job keeps billing if it throws. Vercel functions default to 300 s, so wait in a worker.
- Sume wave_size_hint and a Worker subrequest limit: submit in waves
A Worker fan-out of Sume jobs hits 50 subrequests on Free. Size each wave from generation_limits, not from the hint alone, and stop at queue_capacity_remaining.
- Sume webhook receiver: return 204 for an unknown event, not a 500
Route Sume webhook events through a handler table, answer 204 for any event you do not know, and keep the webhook.test event out of your job table. Node sample.
- A Sume webhook receiver in plain Python WSGI, no framework
A 28-line wsgiref app that reads the raw body, verifies sume-v1 with a rotation-safe check, refuses an empty secret, and answers 204. Tested with curl.
Written by Sume