Port a fal queue submit and poll loop to Sume /v1/videos in Python
A fal queue client posts to queue.fal.run, polls a status URL and fetches a result. The Sume version is 21 lines of Python on /v1/videos. Field map, traps.

A fal queue client submits to queue.fal.run, polls a status URL and then fetches a result URL; the Sume equivalent submits to POST https://api.sume.com/v1/videos, polls the polling_url it returns and downloads from unsigned_urls[0]. The loop is about the same length. What changes is the auth scheme, the model id and the status words.
fal's steps are from its Queue page, read 2026-10-10. Sume's are from the Videos API and Jobs and results docs.
Field map
The fal page shows POST https://queue.fal.run/{model-id} with an Authorization: Key $FAL_KEY header. Status comes from GET .../requests/{id}/status, the result from GET .../requests/{id}, and a cancel from PUT .../cancel. Statuses are IN_QUEUE, IN_PROGRESS and COMPLETED. A webhook is set with webhook_url or the ?fal_webhook= query parameter.
| Step | fal queue | Sume /v1/videos |
|---|---|---|
| Submit | POST https://queue.fal.run/{model-id} | POST https://api.sume.com/v1/videos |
| Auth header | Authorization: Key $FAL_KEY | Authorization: Bearer $SUME_API_KEY |
| Pick the model | In the URL path | model in the body, a bare id such as seedance-2.5 |
| Status words | IN_QUEUE, IN_PROGRESS, COMPLETED | pending, in_progress, completed, failed, cancelled |
| Where to poll | A status URL under the request id | polling_url in the submit response |
| Fetch the output | Separate result URL | unsigned_urls[0], a content URL on the same API |
| Webhook | webhook_url or ?fal_webhook= | callback_url in the body, HTTPS only |
| Retry safety | Not read from the page | Idempotency-Key header returns the original job |
The Sume loop
This runs against the documented route. It sends an Idempotency-Key so a retry of the submit returns the same job, polls with a bounded deadline, and sends the API key when it downloads the content URL.
import os, time, requests
BASE = "https://api.sume.com"
AUTH = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}
def run(prompt, key):
body = {"model": "seedance-2.5", "prompt": prompt, "duration": 5,
"resolution": "720p", "aspect_ratio": "9:16"}
r = requests.post(f"{BASE}/v1/videos", json=body, timeout=30,
headers={**AUTH, "Idempotency-Key": key})
r.raise_for_status()
url = r.json()["polling_url"]
deadline = time.time() + 20 * 60
while time.time() < deadline:
time.sleep(10)
s = requests.get(url, headers=AUTH, timeout=30).json()
if s["status"] == "completed":
v = requests.get(s["unsigned_urls"][0], headers=AUTH, timeout=300)
v.raise_for_status()
return v.content
if s["status"] in ("failed", "cancelled"):
raise RuntimeError(s.get("error") or s["status"])
raise TimeoutError("still running; keep the job id, do not resubmit")
if __name__ == "__main__":
open("out.mp4", "wb").write(run("A paper boat on a pond", "fal-port-001"))Traps when you port
Two spellings differ: this route says cancelled with two Ls, while job webhooks and the jobs API say canceled. If you share one status handler between the poll route and the webhook, normalise them first. Second, the request is stricter than fal's: seed, size and a non-empty provider.options are refused with 400 on this route, so remove them from payloads you copy over. Third, the duration range belongs to the model; Seedance 2.5 takes 4 to 30 seconds.
On timeouts, never resubmit a paid request because your own process gave up. Keep the job id, poll again, and if you must repeat the submit, send the same Idempotency-Key.
When fal is the better fit
fal exposes many models behind per-model endpoints and fal's own client libraries, and its model list is its own. If you depend on a model that Sume's catalog does not list, the port is not possible, whatever the loop looks like. Read GET /v1/video-router/models first and port only the calls whose model id appears there.
Sources
Related posts
More in Developers
- Port an ElevenLabs text-to-speech call to Sume TTS 1.0, field by field
ElevenLabs puts the voice in the URL; Sume TTS 1.0 takes it in the body and rejects model_id. A field map and a tested mapper function.
- Preview a HyperFrames composition as PNG stills with the Sume API
POST /v1/hyperframes-previews returns 1-6 PNG stills of a composition without rendering an MP4. Request shape, polling, and what the frames are not.
- Edit several Format files in one commit with a change set
One PUT to the Sume Format contents root commits many files at once. Learn what a change set keeps, why it cannot delete, and how If-Match guards the package.
- Python 3.14 uuid.uuid7() as a Sume Idempotency-Key: when it is safe
uuid.uuid7() is new in Python 3.14 and makes a tidy Idempotency-Key for Sume submits, if you generate it once per intent and store it. A runnable stdlib sample.
Written by Sume