Text-to-video API in Python: submit, poll, and download

Call Sume's text-to-video API from Python with Requests: POST /v1/videos, poll with timeouts, then stream the MP4 the content route redirects to.

5 min readSume
All posts

To call a text-to-video API from Python, POST a JSON body with model and prompt to https://api.sume.com/v1/videos with Requests, poll the polling_url it returns until status is completed, then GET unsigned_urls[0] with the same Authorization: Bearer header and stream the redirected MP4 to disk. Put a timeout on every call.

Sume facts come from the Video Generation and Jobs and results docs; Requests behavior comes from its Quickstart and Advanced Usage pages, read 2026-09-27. Sume's docs cover a TypeScript SDK, @sume-com/sdk; their own Python sample makes plain HTTPS calls with Requests, and so does this post. The curl version of this flow is the Sume API quickstart.

How do I submit a text-to-video job with Requests?

Only model and prompt are required. Send sume/auto to let Sume pick the model, or pin a bare catalog id from GET /v1/videos/models. Pass the body with json=: Requests encodes it, and its docs point to json= when you need the Content-Type: application/json header that data=json.dumps(...) does not add. The submit answers 202 with id, polling_url, status: "pending", and model.

  • Send an Idempotency-Key header. If the POST times out or the connection drops, resend with the same key: a replay returns the original job instead of starting a second paid one.
  • Requests does not retry failed connections by default. Its docs show urllib3's Retry mounted on a Session with POST among the allowed methods; if you do that, keep the same key on every attempt. Sume's rule is not to retry unsafe submits without an Idempotency-Key.
  • Call raise_for_status(). Requests' docs note that a decodable JSON body does not mean the call succeeded, and Sume's errors are JSON too, under an error object with a code and a request_id. The quickstart lists the common first-call errors.

How long should the poll loop wait?

Poll the polling_url with the same header about every 30 seconds, as the docs suggest, and stop on completed, failed, or cancelled: this route spells the canceled state with two l's. Typical wait times are in how long AI video generation takes.

  • Pass timeout= on every call. If no timeout is set, Requests does not time out, and its docs warn that a program can hang indefinitely.
  • timeout is not a limit on the whole download. It fires when no bytes arrive for that many seconds, so one value suits a small poll and a large MP4. A tuple such as (3.05, 27) sets the connect and read timeouts separately.
  • Keep your own overall deadline. The Sume docs call 20 minutes reasonable for video, and a client-side timeout does not cancel the job: it keeps running and still bills. Store the id and resume polling later.
  • Reads and writes have separate per-key rate budgets, so a poll loop cannot 429 your own submits. If a poll does get 429, back off on retry-after.

Why does downloading unsigned_urls return 401?

An unsigned_urls entry is an API route, not a file: https://api.sume.com/v1/videos/{id}/content?index=0. It takes your key like the submit and the poll, and a request without one gets 401 unauthorized. index defaults to 0 and picks an output when a model returns more than one.

In current code the route answers 302 and redirects to the video's public file on media.sume.com, a different host that needs no Sume key. Requests follows redirects for every verb except HEAD and removes Authorization headers when a redirect goes off-host, so a Bearer key is not sent on to the file's host. Its docs make that promise only for Authorization headers and say custom headers are simply passed on, so an x-api-key header would travel with the redirect: send Authorization: Bearer on this call. To keep the file's URL instead of the bytes, see how to download a generated video.

What does the whole script look like?

Submit, poll, and save in 25 lines. The loop polls before it checks status: a replayed submit returns the original job, which may already be finished, and the submit response never carries unsigned_urls. stream=True defers the body, iter_content writes it in chunks, and the with block makes sure the response is closed even if the loop stops early. The script raises on any error status; in production, treat a 429 on a poll as a pause rather than a failure.

import os, time, requests

AUTH = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}
body = {"model": "sume/auto", "prompt": "A paper boat drifting down a rainy street"}
r = requests.post("https://api.sume.com/v1/videos", json=body, timeout=30,
                  headers={**AUTH, "Idempotency-Key": "paper-boat-v1"})
r.raise_for_status()
job, deadline = r.json(), time.monotonic() + 20 * 60  # giving up does not cancel the job

while True:
    poll = requests.get(job["polling_url"], headers=AUTH, timeout=30)
    poll.raise_for_status()
    job = poll.json()
    if job["status"] in ("completed", "failed", "cancelled"):
        break
    if time.monotonic() > deadline:
        raise TimeoutError(f"{job['id']} is still running; poll it later")
    time.sleep(30)
if job["status"] != "completed":
    raise RuntimeError(job.get("error", job["status"]))
with requests.get(job["unsigned_urls"][0], headers=AUTH, stream=True, timeout=30) as video:
    video.raise_for_status()
    with open("video.mp4", "wb") as f:
        for chunk in video.iter_content(chunk_size=1024 * 1024):
            f.write(chunk)

Which Requests settings matter on each call?

The table sums up the three calls. The Developer API has no SSE or WebSocket stream, so a script either polls or takes a webhook: to skip polling, send callback_url, which must be HTTPS, and Sume POSTs a signed webhook when the job reaches a terminal state. A Python webhook receiver covers the server side.

From Video Generation, the Requests Quickstart, and current API code for the 302, read 2026-09-27.
CallSume answersRequests settings
POST /v1/videos202 with id and polling_urljson=, an Idempotency-Key header, timeout
GET the polling_urlThe job, with statusThe same auth header, timeout, about 30 seconds between polls
GET an unsigned_urls entry302 to the video fileAuthorization: Bearer, stream=True, iter_content, timeout

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume