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.

5 min readSume
All posts

In Node 18 or later, fetch is enough to send reference images to Seedance 2.5: POST to https://api.sume.com/v1/videos with an input_references array, then poll polling_url until the job is completed and read unsigned_urls[0]. The script below does it in 30 lines, in an ES module so top-level await works.

It is the TypeScript twin of the Python version. Both follow the flow in Sume's Video generation docs.

What does the script look like?

Save it as clip.mjs (or clip.ts under a runner such as tsx), set SUME_API_KEY, and swap the two URLs for public HTTPS images.

const headers = {
  Authorization: `Bearer ${process.env.SUME_API_KEY}`,
  "Content-Type": "application/json",
};
const refs = ["https://example.com/hero.png", "https://example.com/product.png"];
const res = await fetch("https://api.sume.com/v1/videos", {
  method: "POST",
  headers,
  body: JSON.stringify({
    model: "seedance-2.5",
    prompt: "@Image1 holds @Image2 and smiles at the camera",
    input_references: refs.map((url) => ({ type: "image_url", image_url: { url } })),
    duration: 8,
    resolution: "720p",
    aspect_ratio: "9:16",
  }),
});
if (!res.ok) throw new Error(`submit failed: ${res.status} ${await res.text()}`);
const job = await res.json();
for (;;) {
  await new Promise((r) => setTimeout(r, 30_000));
  const state = await (await fetch(job.polling_url, { headers })).json();
  if (state.status === "completed") {
    console.log(state.unsigned_urls[0]);
    break;
  }
  if (state.status === "failed" || state.status === "cancelled") {
    throw new Error(JSON.stringify(state.error ?? state.status));
  }
}

How does it behave?

The submit call throws with the status and body on any non-2xx response, so a 400 unsupported_capability for too many references shows its message. The poll loop waits 30 seconds before the first check, which matches the docs' example, and stops on completed, failed or cancelled.

The last line prints the content URL rather than saving a file. That URL needs your bearer token, so fetch it with the same headers object when you want the bytes: the docs' download example uses curl with the Authorization header and --output video.mp4.

Under TypeScript the only changes are types. The poll result is untyped JSON, so declare a small interface with status, unsigned_urls and error rather than using any, and keep the nested input_references shape exact: each entry is { type: "image_url", image_url: { url } }.

If the script runs in a job runner that should not hang, add an AbortSignal.timeout(60_000) to the submit call.

What are the field rules for references?

Reference entries are objects with type and a nested object. Seedance 2.5 on Sume takes images, videos and audio, with a combined cap.

Seedance 2.5 reference request rules on /v1/videos, read 2026-10-02
FieldRule
input_referencesArray of {type, <type>: {url}} objects; max 12 in total
frame_imagesIf present, it takes precedence over input_references (image-to-video)
durationInteger, 4 to 30 on seedance-2.5
resolution480p, 720p or 1080p
aspect_ratioOne of the ratios the model lists in supported_aspect_ratios; send it explicitly

How do you use the generated SDK instead?

Sume publishes an SDK; the SDK docs are the place to check whether your language has a typed client for the video endpoint. A raw fetch call has one advantage: the body is the same JSON as the curl examples, so you can paste a request from the docs, change model, and run it. For a typed client, check that it exposes input_references before you depend on it.

What are the usual mistakes?

Three come up. First, sending frame_images and references together and wondering why the references did nothing; the which-wins post explains it. Second, using a private or expiring URL the provider cannot fetch; use public HTTPS URLs. Third, polling too fast: nothing changes between polls for the first 30 seconds or more, and the docs say generation can take several minutes. Pass callback_url if you would rather be called.

What does the request cost?

The script asks for 8 seconds at 720p and 9:16 with two reference images. Sume estimates video tokens as width x height x seconds x 24 / 1024, so 720p at 8 seconds is 172,800 tokens. The docs do not give a separate charge for reference images, so read the cost on the completed job. A 4-second, 480p test is far cheaper, so run that first and read the cost on the completed job.

If you want 1080p, change resolution; the larger frame means more tokens. And if you need more than 8 seconds, duration accepts any integer from 4 to 30 on seedance-2.5. Past that, join clips; see the extension post.

Where should the key live?

Read SUME_API_KEY from the environment, as the script does, and never put it in a browser bundle: the Authorization: Bearer header is the whole credential. For a web app, call a server route of your own that holds the key and forwards the request, then return the job id to the browser. The API reference lists the auth rules and the job endpoints.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume