Stream a Sume video download in Python: .part file, then rename

Download /v1/videos/{id}/content with requests stream=True, write a .part file and rename on success. A lost 30 s clip costs $1.875 to $17.334 to re-buy.

4 min readSume
All posts

Download a finished Sume clip with GET /v1/videos/{id}/content?index=0, your API key in the header, requests stream=True, and write to a .part file that you rename only after the last chunk. A half-written MP4 that looks complete is the failure to design against, because re-making a 30 s clip costs real money.

What a lost file costs

The content endpoint needs your key, and the poll response lists it in unsigned_urls, so the file is a plain authenticated download once the job is completed. If your process dies mid-write and a later step treats the partial file as done, you pay for the render again.

The figures below are billable USD for the 30 s clips most likely to be downloaded in a batch.

Cost to re-render one 30 s clip, 9:16 for Seedance (read 2026-10-09)
Model and resolutionRate per second30 s total
wan-3.0 480p$0.0625$1.875
wan-3.0 720p$0.125$3.75
wan-3.0 1080p$0.25$7.50
seedance-2.5 720p$0.5778$17.334

The download function

Stream in 1 MiB chunks, so memory stays flat for any clip size. The (10, 60) timeout is connect then read, which stops a stalled socket without cutting a slow but moving one.

import os
import requests

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

def save(job_id, path, index=0):
    url = f"https://api.sume.com/v1/videos/{job_id}/content"
    tmp = path + ".part"
    with requests.get(url, headers=HEADERS, params={"index": index},
                      stream=True, timeout=(10, 60)) as r:
        if r.status_code == 409:
            raise RuntimeError(r.json()["error"]["code"])
        r.raise_for_status()
        with open(tmp, "wb") as f:
            for chunk in r.iter_content(chunk_size=1 << 20):
                f.write(chunk)
    os.replace(tmp, path)
    return path

Gotchas

A 409 means the job is not ready: job_not_completed is retryable, job_failed is not. Poll the job until status is completed before you call the content endpoint, then branch on the error code instead of retrying everything.

Compare the bytes written with the Content-Length header when the server sends one, and keep the job id until the rename succeeded. The docs state no expiry for result URLs, so copy the file to your own storage if you need it long term.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume