Poll a Seedance 2.5 job in TypeScript: deadline and failed states
A TypeScript submit-poll-download loop for a 30-second Seedance 2.5 render on Sume: the 30 s interval, a 20-minute deadline, and the three end states.

To render a 30-second Seedance 2.5 clip from TypeScript, POST to /v1/videos, then poll the polling_url every 30 seconds until the status is completed, failed or cancelled. Put a deadline around the loop so a stuck job cannot hold your process forever.
The code below is Node 18+ with the built-in fetch. The Sume docs recommend a moderate polling interval (30 seconds in the examples) and say video generation usually takes from 30 seconds to several minutes, depending on the model and parameters.
The loop
Submit once, keep the polling_url from the response, and download from the first entry of unsigned_urls when the job completes.
const key = process.env.SUME_API_KEY;
if (!key) throw new Error("SUME_API_KEY is not set");
const headers = { Authorization: "Bearer " + key, "Content-Type": "application/json" };
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
export async function render(prompt: string): Promise<ArrayBuffer> {
const res = await fetch("https://api.sume.com/v1/videos", {
method: "POST",
headers: { ...headers, "Idempotency-Key": "tour-001" },
body: JSON.stringify({ model: "seedance-2.5", prompt, duration: 30,
resolution: "720p", aspect_ratio: "16:9" }),
});
if (!res.ok) throw new Error("submit failed: " + res.status);
const job = await res.json();
const deadline = Date.now() + 20 * 60_000;
while (Date.now() < deadline) {
await sleep(30_000);
const s = await (await fetch(job.polling_url, { headers })).json();
if (s.status === "completed") {
const v = await fetch(s.unsigned_urls[0], { headers });
return v.arrayBuffer();
}
if (s.status === "failed" || s.status === "cancelled")
throw new Error(s.status + ": " + (s.error ?? "no detail"));
}
throw new Error("gave up after 20 minutes; job " + job.id + " may still finish");
}What each status means
The docs list five job statuses. A long render spends most of its life in the first two.
| Status | Meaning | What the loop does |
|---|---|---|
| pending | Submitted and queued | Keep polling |
| in_progress | Generation running | Keep polling |
| completed | Video is ready | Download from unsigned_urls[0] |
| failed | Generation failed; see the error field | Throw with the error text |
| cancelled | Job cancelled before completion | Throw |
Why 30 seconds and a deadline
A 30-second render is not instant, and polling every second only spends your request budget. The docs' own examples sleep 30 seconds between polls. A 20-minute deadline is an app-side choice, not a Sume limit: pick a number that matches your queue, and when it passes, stop waiting but do not assume the job died. The error message in the sample keeps the job id so you can look it up later through the jobs endpoints, GET /v1/jobs/{id}/status and GET /v1/jobs/{id}/result, which the docs say show the same job.
The reserve is worth knowing before you loop. At submit Sume reserves the list price x 1.25, so a 30-second 720p 16:9 request holds $17.34 of the wallet while it runs, and $42.65 at 1080p (read 2026-10-05).
Retry safely
The sample sends an Idempotency-Key. If your process dies after the POST but before you saved the job id, run the same request with the same key: the docs say a replay returns the original job. Use a key per intended video, not per attempt, or you will pay twice for the same clip.
Do not retry a failed job with the same key expecting a fresh render; a replay returns the original job. Change the key when you want a new take.
Alternatives to polling
If you do not want a loop at all, send callback_url (HTTPS) in the request and Sume POSTs to it when the job reaches a terminal state. The docs say the body is signed and sent with x-sume-webhook-timestamp and x-sume-webhook-signature headers; verify the signature before trusting it. For a single script, polling is simpler. For a service handling many jobs, the webhook saves requests.
Whichever you use, check the catalog first: GET /v1/videos/models lists the durations and resolutions each model accepts, and for seedance-2.5 the Video Router docs give 4-30 seconds at 480p, 720p and 1080p.
Sources
Related posts
More in Developers
- Poll an H3 Max video job in Python: backoff, terminal, result
Submit minimax-h3-max to the Video Router, poll GET /v1/jobs/:id/status with backoff until terminal, then fetch the result. A runnable 25-line Python script.
- Poll or webhook for Wan 3.0 30-second jobs: use both on Sume
A webhook tells you when a Wan 3.0 job ends; polling is the backup when delivery fails. Sume retries a webhook up to 10 times at 30 s spacing. How to wire both.
- Polling Gemini Omni jobs on Sume: statuses, backoff, Python example
Poll GET /v1/videos/{id} with backoff: pending, in_progress, completed, failed, cancelled. Python asyncio example for gemini-omni-flash-1.1 from 360p to 4K.
- Polling or webhook for 1,000 video jobs: count the requests
How many requests does a 1,000-job video batch need with polling or webhooks? A Python counter for poll schedules, plus Sume's per-plan read budgets.
Written by Sume