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.

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.
| Resolution | Provider list per second | Sume per second | 30 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.costat 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
- Wan 3.0 on Alibaba: 300 requests a minute across all regions, planned
Alibaba caps Wan 3.0 at 300 requests per minute across all six regions, so a region switch does not add headroom. How to plan a batch of 30 s jobs.
- Wan 3.0 and Seedance 2.5 auto duration on fal: send seconds on Sume
fal lets Wan 3 pick its own length and Seedance 2.5 accepts auto. On Sume, send an integer duration so the price is known before you submit.
- Wan 3.0 on Sume: one id, three modes, and which field picks each
fal lists three Wan 3 endpoints. Sume has one id, wan-3.0, and picks text, image or reference-to-video from frame_images and input_references.
- Wan 3.0 sync mode waits 30 seconds: a 30-second clip needs async
Sume's sync mode holds a request at most 30 seconds, so a 30 second Wan 3.0 clip needs async or a webhook. What the timeout means and how to avoid paying twice.
Written by Sume