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.

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.
| Cause | Result | Fix |
|---|---|---|
| More than the listed reference limits | 400 error naming the limit | Trim the list to what the catalog allows |
| Unknown model id | model_not_found | Use a bare catalog id such as seedance-2.5 |
| Reference URL not public HTTPS | Failed job, see the error field | Host 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
- Seedance 2.5 reference images in TypeScript with Node fetch
A Node 18+ ESM script: submit reference images to seedance-2.5 on Sume's /v1/videos, poll every 30 s with fetch, print the video URL. Rules and mistakes.
- Seedance "accepts at most 12 input_references": mixes that fit
Sume returns unsupported_capability when images + videos + audio on a Seedance request exceed 12. Which mixes pass, what fal limits per type, and how to trim.
- One image to Seedance: reference, or first frame?
On Sume a single reference image with no frame field is priced and routed as reference-to-video; add a first frame to get image-to-video. How to choose.
- Cartesia sonic-3.6-2026-08-27 snapshot: which id Sume accepts
Cartesia's dated snapshot ids never change, but Sume's TTS Router lists only sonic-3.6, 3.5, 3, latest and preview. Here is what that means for repeat takes.
Written by Sume