Seedance 2.5 reference images in Python with asyncio and httpx

A runnable Python script: send reference images to seedance-2.5 on Sume's /v1/videos, poll every 30 s with asyncio, save the MP4, and handle the errors.

5 min readSume
All posts

To send reference images to Seedance 2.5 from Python, POST to https://api.sume.com/v1/videos with model: "seedance-2.5" and an input_references array of image_url objects, then poll the polling_url until the status is completed and download unsigned_urls[0]. The script below does that with asyncio and httpx in 30 lines.

It follows the submit, poll and download flow in Sume's Video generation docs, which use a 30-second poll interval because generation takes from tens of seconds to several minutes.

What does the script look like?

Install httpx, export SUME_API_KEY, replace the two example URLs with public HTTPS images, and run it. The async code is wrapped in asyncio.run(main()), so it works as a plain script.

import asyncio, os
import httpx
HEADERS = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}
REFS = ["https://example.com/hero.png", "https://example.com/product.png"]

async def main():
    body = {
        "model": "seedance-2.5",
        "prompt": "@Image 1 holds @Image 2 and smiles at the camera",
        "input_references": [{"type": "image_url", "image_url": {"url": u}} for u in REFS],
        "duration": 8, "resolution": "720p", "aspect_ratio": "9:16",
    }
    async with httpx.AsyncClient(headers=HEADERS, timeout=60, follow_redirects=True) as client:
        resp = await client.post("https://api.sume.com/v1/videos", json=body)
        resp.raise_for_status()
        job = resp.json()
        while True:
            await asyncio.sleep(30)
            poll = await client.get(job["polling_url"])
            poll.raise_for_status()
            state = poll.json()
            if state["status"] == "completed":
                break
            if state["status"] in ("failed", "cancelled"):
                raise SystemExit(state.get("error", state["status"]))
        video = await client.get(state["unsigned_urls"][0])
        with open("clip.mp4", "wb") as f:
            f.write(video.content)

asyncio.run(main())

What does each part do?

input_references is built from the REFS list, one image_url object per URL; add more entries for more references (the catalog lists up to 9 images, and images, videos and audio together up to 12). The prompt names them by position with @Image 1 and @Image 2, per the tag post. duration, resolution and aspect_ratio are set explicitly so the request does not depend on defaults.

The submit call returns id, polling_url and status. The loop sleeps first, then polls, and stops on completed; failed and cancelled are the other terminal states in the docs. follow_redirects=True covers the case where the content URL redirects to a hosted file.

To add a reference video or audio clip, append a video_url or audio_url entry to the same list; the JSON shapes post shows all three.

If a job fails, the poll response carries an error field; the script exits with it so the message reaches your terminal. Seedance can refuse reference images of real human faces in some cases, which a separate post covers.

What errors will you see?

raise_for_status() turns a 4xx into an exception whose response body carries the Sume error. These are the ones you will meet with references.

Common errors on a Seedance reference request, read 2026-10-02
CauseResultFix
More than the listed reference limits400 error naming the limitTrim the list to what the catalog allows
Unknown model idmodel_not_foundUse a bare catalog id such as seedance-2.5
Reference URL not public HTTPSFailed job, see the error fieldHost the file at a public HTTPS URL

Should you poll or use a webhook?

Polling is the simplest and fine for one-off scripts. For many jobs, pass callback_url (HTTPS only) and let Sume POST to you; the payload is signed with x-sume-webhook-signature and x-sume-webhook-timestamp, and the callback post covers verification. Either way, keep the job id: the Jobs and results docs describe the status and result endpoints you can use to recover a job after a crash.

What this script does not do is retry a failed submit. A retried POST can create a second paid job, and the way to make retries safe is the Idempotency-Key header that Sume's examples send on other endpoints; add one per logical request before you wrap this in retry logic.

Keep the timeout on the client at 60 seconds or more for the submit call; the long wait is in the polling loop, not in any single request. If you run many scripts at once, stagger the polls so they do not all hit the API on the same second.

What does it cost?

Seedance bills per video token at provider list times 1.25, so cost depends on duration and resolution. Read the live rate from GET /v1/video-router/models before a large run, and test with resolution: "480p" and duration: 4 first. Reference-image pricing is covered in the price-effect post.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume