Deno: a 30-second Wan 3.0 video with fetch and top-level await

A short Deno script that submits a 30-second wan-3.0 job, polls to completion and writes clip.mp4, using only fetch and no npm packages.

3 min readSume
All posts

Deno can run a full Wan 3.0 job with no dependencies: fetch for the three calls, top-level await for the flow, and Deno.writeFile for the MP4. The script below submits wan-3.0 for 30 seconds at 720p, polls every five seconds, and exits with an error if the job ends as anything but completed.

The reserve for that request is $3.75. The Video generation page documents the submit, the pending / in_progress / completed / failed / cancelled statuses and the content endpoint used here.

clip.ts

Run it with deno run --allow-env --allow-net --allow-write clip.ts. The three permission flags are all it needs.

const base = "https://api.sume.com/v1/videos";
const key = Deno.env.get("SUME_API_KEY");
if (!key) throw new Error("SUME_API_KEY is empty");
const auth = { Authorization: `Bearer ${key}` };

const res = await fetch(base, {
  method: "POST",
  headers: { ...auth, "Content-Type": "application/json", "Idempotency-Key": "wan-deno-001" },
  body: JSON.stringify({
    model: "wan-3.0", prompt: "A paper boat drifting down a rain gutter, macro lens",
    duration: 30, resolution: "720p", aspect_ratio: "16:9",
  }),
});
if (!res.ok) throw new Error(`submit failed: ${res.status} ${await res.text()}`);
const { id } = await res.json();

let status = "pending";
while (status === "pending" || status === "in_progress") {
  await new Promise((r) => setTimeout(r, 5000));
  status = (await (await fetch(`${base}/${id}`, { headers: auth })).json()).status;
}
if (status !== "completed") throw new Error(`job ended as ${status}`);
const mp4 = await fetch(`${base}/${id}/content?index=0`, { headers: auth });
await Deno.writeFile("clip.mp4", new Uint8Array(await mp4.arrayBuffer()));

Why it is written this way

  • The key is read from SUME_API_KEY and the script throws if it is empty, so a missing variable never turns into an unauthenticated request.
  • The submit checks res.ok and prints the response body. A 402 insufficient_credits body names the shortfall, which is the quickest way to learn the hold was larger than the balance.
  • The polling loop has no timeout on purpose; add one if the script lives inside a scheduled task with a hard wall-clock limit.

Cost by resolution

Change resolution and nothing else in the script:

Sume list price, read 2026-10-08
resolution30-second reserve
480p$1.875
720p$3.75
1080p$7.50

Smoke test before the 30-second run

Run the script once with "duration": 2 and "resolution": "480p" before you trust it. Wan 3.0 accepts 2 seconds, and that job reserves $0.125, against $3.75 for the full 720p clip. It proves the key, the polling and the file write for the price of a coffee-sized fraction of one real run.

Scaling to a list of prompts

For several prompts, wrap the submit in a function and keep a unique Idempotency-Key per prompt, such as the prompt's slug plus its resolution. Submit them in a small group and poll each id; the Sume plan limits decide how many can be accepted at once, so a 429 is a signal to wait and retry with the same key.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume