Download a finished Seedance or Kling MP4 from the Sume API

Two ways to get the file: GET /v1/videos/{id}/content with your key, or the hosted artifact URL from /v1/jobs/{id}/result. A Python download script.

5 min readSume
All posts

To download a finished Seedance or Kling video from Sume, poll GET /v1/videos/{jobId} until the status is completed, then request GET /v1/videos/{jobId}/content?index=0 with your API key and save the bytes. The poll response also lists unsigned_urls, and the job result at /v1/jobs/{id}/result returns a Sume-hosted artifact URL on media.sume.com. Either route gives you the MP4.

This post shows both, a short Python script that streams the file to disk, and what to avoid: parsing artifact URLs, or assuming a video comes back in a particular container.

Which URL should you download from?

The video generation docs describe the content endpoint as the way to fetch the bytes once the job is completed; the index query parameter defaults to 0 and exists for models that generate more than one output. Their cURL example sends the Authorization header with the request, so send it too. Do not assume the URL in unsigned_urls can be fetched anonymously.

The job-centric route is the alternative: GET /v1/jobs/{id}/result returns artifacts with an id, a type of video, a url on media.sume.com and a content_type of video/mp4. The docs say to treat artifact URLs as opaque, so do not parse the path for workspace, job or provider identifiers.

Two ways to fetch a finished video (Sume docs, read 2026-10-02)
RouteCallUse it when
Content endpointGET /v1/videos/{jobId}/content?index=0 with AuthorizationYou submitted on /v1/videos and want the bytes directly
Job resultGET /v1/jobs/{id}/result, then the artifact urlYou store the hosted URL, or you submitted on another video route
Webhook payloadpayload.artifacts on job.completedYou use callback_url and want to skip polling

What does the download script look like?

The script below polls the job, then streams the content to a file in chunks so a 30-second 1080p clip never sits fully in memory. It sleeps between polls, stops on failed or cancelled, and fails loudly on any HTTP error. Poll about every 30 seconds, which the docs suggest; shorter intervals just add read-rate pressure.

import json, os, sys, time, urllib.request

KEY = os.environ["SUME_API_KEY"]
JOB = sys.argv[1]
BASE = "https://api.sume.com/v1/videos/" + JOB
AUTH = {"Authorization": "Bearer " + KEY}

def get(url):
    return urllib.request.urlopen(urllib.request.Request(url, headers=AUTH), timeout=60)

while True:
    status = json.load(get(BASE))["status"]
    print(status)
    if status == "completed":
        break
    if status in ("failed", "cancelled"):
        sys.exit("job ended: " + status)
    time.sleep(30)

with get(BASE + "/content?index=0") as resp, open(JOB + ".mp4", "wb") as out:
    while chunk := resp.read(1 << 20):
        out.write(chunk)
print("saved", JOB + ".mp4")

What should you not rely on?

Do not rely on the provider's own temporary URL lifetime. Sume serves the clip itself, so the retention rules of the upstream vendor are not your constraint; still, keep your own copy of anything that matters, since the docs make no promise about how long a hosted artifact stays available.

Note the spelling of the status values: the video route uses cancelled in its job status table, while job webhooks use job.canceled. The script handles the route it polls. Also, usage.cost on the completed poll response is the Sume billable amount, so you can log the price of each download-ready clip next to the file name.

What about long clips and slow jobs?

A clip that has not completed is not an error. Seedance 2.5 clips up to 30 seconds can take several minutes, and a job can sit in a queue behind your plan's concurrency limit. Keep polling, or use callback_url and a webhook so your server is notified, and fall back to polling if the event never arrives. If a job fails, the reserved amount is refunded and the error field on the poll response says why.

If your pipeline fetches many videos, download from your own storage after the first copy rather than from Sume repeatedly. For queueing and the full state machine, see Jobs and results.

A last practical point: record the job id, the model id and the usage.cost beside every saved file. When a client asks which clip came from which prompt, or what a batch cost, you can answer from your own log instead of reconstructing it from the dashboard, and an idempotency key per clip lets you replay a submit without paying twice.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume