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.

5 min readSume
All posts

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.

Sume video job statuses, from the video generation docs read 2026-10-05
StatusMeaningWhat the loop does
pendingSubmitted and queuedKeep polling
in_progressGeneration runningKeep polling
completedVideo is readyDownload from unsigned_urls[0]
failedGeneration failed; see the error fieldThrow with the error text
cancelledJob cancelled before completionThrow

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

All Developers posts

Written by Sume