Download a 4K Gemini Omni clip from /content: redirect, 409 and retry
GET /v1/videos/{id}/content answers 302 to the file, or 409 job_not_completed while rendering. Python that reads the redirect without leaking your API key.

To download a finished Sume video, call GET /v1/videos/{id}/content?index=0. It answers with a redirect to the artifact, or with 409 job_not_completed while the job still runs. A safe client reads the Location header itself and fetches the file without sending your Authorization header to the media host.
What a 4K Omni clip changes
Gemini Omni Flash 1.1 is in the Sume catalog at 3 to 10 seconds with 360p, 720p, 1080p and 4K, 16:9 or 9:16, and native synced audio. A 4K file is large. Google's own Omni docs say that videos larger than 4MB should be delivered by URI instead of inline data, so plan for a download step, not a base64 field.
On Sume the poll response carries unsigned_urls, and each entry points at the content route of the same job. The route is authenticated with your key and redirects to the stored artifact.
There is a second reason to avoid inline delivery in your own design. A 4K clip held in memory, or encoded as text, multiplies your peak memory per worker. Streaming the redirect target straight to disk, as the code below does with copyfileobj, keeps a worker at a flat memory profile no matter how many 4K clips it fetches in a day.
The three answers you will see
The content route has a small, stable set of outcomes. Treat them differently, because only one is worth retrying.
| HTTP | Code | Meaning | Retry? |
|---|---|---|---|
| 302 | none | Redirect to the artifact URL | Follow once |
| 409 | job_not_completed | Job still running | Yes, after a poll |
| 409 | job_failed | Terminal failure | No, read the job error |
| 404 | job_not_found | Unknown or foreign job | No |
Download without forwarding the key
Python's urllib follows redirects by default and carries your headers along. To keep the bearer token on api.sume.com only, turn redirects off, read Location, then fetch it bare.
import os, shutil, sys, time, urllib.error, urllib.request
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, *a, **k):
return None
def download(job_id, out="clip.mp4", tries=6):
url = f"https://api.sume.com/v1/videos/{job_id}/content?index=0"
req = urllib.request.Request(url, headers={"Authorization": "Bearer " + os.environ["SUME_API_KEY"]})
opener = urllib.request.build_opener(NoRedirect)
for n in range(tries):
try:
opener.open(req, timeout=30)
except urllib.error.HTTPError as e:
if e.code in (301, 302, 303, 307, 308):
with urllib.request.urlopen(e.headers["Location"], timeout=120) as r, open(out, "wb") as f:
shutil.copyfileobj(r, f)
return out
if e.code == 409 and b"job_not_completed" in e.read():
time.sleep(min(5 * 2 ** n, 60))
continue
raise
raise TimeoutError("still rendering after retries")
print(download(sys.argv[1]))
Why 409 job_not_completed is not an error
Sume documents that code as retryable with a next action of polling status. That is the opposite of job_failed, which carries retryable false and tells you to inspect events. A client that retries both will loop forever on a render that never existed, so branch on the code, not on the status number.
Run it as python download.py job_id after your poll loop reports completed. If you call it early, the backoff above waits 5, 10, 20, 40, 60 and 60 seconds after each failed try.
The 409 retry loop has a ceiling on purpose. Six tries with doubling waits is about three minutes of patience. If a job is still not complete after that, you are not in a download problem, you are in a poll problem, so go back to the status route and let the job tell you its state before you ask for bytes again.
Checks before you ship it
- Store the job id, not the redirect URL. The redirect target is a delivery detail.
- Pass index only when the model makes several outputs. The default is 0.
- Download promptly and keep your own copy. Retention differs by vendor, and the file you need next month should live in your storage.
- Edit mode for Omni takes a source video of 10 seconds or less on Google's side, so trim inputs before you submit.
Sources
Related posts
More in Developers
- Does Dr. split a Sume STT segment? Abbreviations and sentence mode
Segmentation tests each token for a final period, so a token like Dr. or U.S. can end a segment early. Merge on your side with a small abbreviation list.
- Draft with sume/auto, finish on a pinned model: why the look differs
sume/auto never says which model ran, so a draft from it is no preview for a pinned final. Draft and finish on the same catalog id, and where Auto fits.
- 'duration_seconds must match duration': send one field, not two
Video Router accepts duration or duration_seconds as the same value. Send both with different numbers and you get a 400. Send one, or send two equal ones.
- Edit a finished Omni clip with video_url instead of re-rolling it
Send an existing clip as video_url to gemini-omni-flash-1.1 on Sume with an edit prompt. Resolution defaults to 720p; send no duration or aspect_ratio.
Written by Sume