Wan 3.0 for 30 seconds in Node: submit, poll, save the MP4

A runnable Node 18+ script for Sume's /v1/videos: submit wan-3.0 at 30 seconds, poll with a deadline, stream the MP4 to disk. Price: $1.88 at 480p.

5 min readSume
All posts

To render a 30-second clip with Wan 3.0 from Node, POST /v1/videos with model: "wan-3.0" and duration: 30, poll the polling_url until the status is completed, then stream GET /v1/videos/{id}/content?index=0 to a file. The script below does all three with built-in fetch, needs Node 18 or newer, and gives up after a deadline you choose.

Wan 3.0 accepts 2 to 30 seconds on Sume. At 480p a 30-second clip costs about $1.88, at 720p $3.75 and at 1080p $7.50, because Sume bills the provider list rate times 1.25.

The statuses you will see

The /v1/videos surface uses its own status words: pending while the job waits in the queue, in_progress while it renders, then completed, failed, or cancelled. The word is spelled with two l's here, which differs from the canceled you see on /v1/jobs. A loop that only checks for completed and failed will spin forever on a cancelled job, so the script treats every status that is not pending or in progress as final.

Wan 3.0 price for a 30-second clip on Sume, list x 1.25, Sume docs read 2026-10-05
ResolutionProvider list per secondSume per second30 seconds
480p$0.05$0.0625$1.875
720p$0.10$0.125$3.75
1080p$0.20$0.25$7.50

The script

Save it as wan.mjs, set SUME_API_KEY, and run node wan.mjs. The download needs the same Authorization header as the poll, because unsigned_urls are not public links.

import { createWriteStream } from "node:fs";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";

const base = "https://api.sume.com/v1/videos";
const headers = { Authorization: "Bearer " + process.env.SUME_API_KEY,
  "Content-Type": "application/json" };

const res = await fetch(base, { method: "POST", headers, body: JSON.stringify({
  model: "wan-3.0", duration: 30, resolution: "480p",
  prompt: "A paper boat drifts down a rain-soaked street at dusk" }) });
if (!res.ok) throw new Error(res.status + " " + await res.text());
const job = await res.json();

const deadline = Date.now() + 20 * 60_000;
let s = job;
while (["pending", "in_progress"].includes(s.status)) {
  if (Date.now() > deadline) throw new Error("deadline; job " + job.id + " still runs");
  await new Promise((r) => setTimeout(r, 15_000));
  s = await (await fetch(job.polling_url, { headers })).json();
  console.log(s.status);
}
if (s.status !== "completed") throw new Error(s.status + " " + JSON.stringify(s.error));

const out = await fetch(base + "/" + job.id + "/content?index=0", { headers });
if (!out.ok) throw new Error("download " + out.status);
await pipeline(Readable.fromWeb(out.body), createWriteStream("wan.mp4"));
console.log("saved wan.mp4, cost", s.usage?.cost);

Choices worth keeping

  • The deadline error names the job id. A timeout in your script does not cancel the render, and it still bills, so log the id and resume later.
  • A 15-second poll is gentle for a render that takes minutes. Slow it further when you run many clips at once.
  • The script reads usage.cost at the end, which is the Sume billable amount. Compare it with the table to catch a wrong resolution.
  • A non-2xx on the first call is thrown with the body, so a 402 or 429 shows its error code instead of a silent failure.

When to use a webhook instead

For one clip on a laptop, polling is fine. For a queue of them, pass callback_url (HTTPS only) on submit and let Sume call you. A receiver that verifies the signature and answers fast is covered in the webhook posts linked below, and the poll loop stays as a reconcile fallback.

Running it for real

Run the script once with a 5-second clip before you trust it with 30. Wan 3.0 accepts 2 to 30 seconds, so change duration to 5 and the cost is a fraction of a dollar at 480p. When the loop, the download and the file all work, put the duration back.

Expect the render itself to take minutes, not seconds, which is why the loop sleeps and why the deadline is set in minutes. If the deadline fires, the error names the job so you can fetch the result later with the same key. Do not rerun the whole script in that case, because it would submit again.

If you want the same flow with Seedance 2.5, only the model name and the price table change; the endpoints, the statuses and the download call are identical.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume