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.

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-Keyheader. 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
Retrymounted on a Session withPOSTamong the allowed methods; if you do that, keep the same key on every attempt. Sume's rule is not to retry unsafe submits without anIdempotency-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 anerrorobject with acodeand arequest_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. timeoutis 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
idand resume polling later. - Reads and writes have separate per-key rate budgets, so a poll loop cannot
429your own submits. If a poll does get429, back off onretry-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.
| Call | Sume answers | Requests settings |
|---|---|---|
POST /v1/videos | 202 with id and polling_url | json=, an Idempotency-Key header, timeout |
GET the polling_url | The job, with status | The same auth header, timeout, about 30 seconds between polls |
GET an unsigned_urls entry | 302 to the video file | Authorization: Bearer, stream=True, iter_content, timeout |
Sources
Related posts
More in Integrations
- Trigger.dev wait for webhook: waitpoint tokens for Sume runs
Create a Trigger.dev waitpoint token, send token.url as a Sume run's webhook_url, and wait.forToken() returns when Sume POSTs the run's result.
- Vercel AI SDK: generate video with a Sume API tool call
Generate video from the Vercel AI SDK with a tool() that calls Sume's POST /v1/videos on your server, returns the job id, and polls for the clip.
- Vercel Cron Jobs: call the Sume API daily without duplicates
A Vercel cron job sends a GET to your route, which calls the Sume API with a date-based Idempotency-Key, so a duplicate invocation cannot bill twice.
- Vercel function timeout on video generation: use a webhook
A Vercel Function stops at 300 seconds by default; a Sume video run takes minutes. Submit with a webhook_url, return, then verify the signed POST.
Written by Sume