Deno fetch: 25 s Wan 3.0 clip costs $3.125, 20-minute deadline
Deno script: submit a 25-second Wan 3.0 clip to Sume at 720p ($3.125), poll with backoff, stop at a 20-minute deadline. Run with deno run -A.

To call a 25-second Wan 3.0 video from Deno, POST to https://api.sume.com/v1/videos with model wan-3.0, duration 25 and resolution 720p, expect a 202 with a polling_url, and poll that URL until the status is completed. At 720p the list price is $0.125 per output second, so 25 seconds is 25 x 0.125 = $3.125. The script below adds the two things most copied examples leave out: a hard deadline for the whole wait and a poll delay that grows instead of hammering the status route.
No SDK is needed. Deno ships fetch, AbortSignal and top-level await, so the file is one script. Save it as clip.ts, export SUME_API_KEY, and run deno run -A clip.ts.
What the request asks for
wan-3.0 accepts 2 to 30 seconds, so 25 is inside its range. The Idempotency-Key header makes a retried submit return the original job instead of a second paid one; the key in the sample is a fixed string, so change it for each new clip you want to pay for. The table lists each field and the number behind it.
| Field | Value in the sample | Why |
|---|---|---|
| model | wan-3.0 | Bare catalog id; Sume ids have no org prefix |
| duration | 25 | wan-3.0 accepts 2-30 s |
| resolution | 720p | List price $0.125 per second |
| aspect_ratio | 16:9 | Subset reported by GET /v1/videos/models |
| Cost | 25 x 0.125 = $3.125 | Per-second price is linear inside the valid range |
| Same clip at 1080p | 25 x 0.25 = $6.25 | 1080p is $0.25 per second |
A poll loop with a deadline
Two timeouts do different jobs. AbortSignal.timeout(20 * 60_000) is the deadline for the whole wait; the script checks it with throwIfAborted() before each poll, so a job that sits in a queue for longer than 20 minutes ends your script with a clear error instead of an endless loop. The per-request signal (15 seconds) only protects one GET from a hung connection.
The delay starts at 2 seconds and is multiplied by 1.5 after every poll, capped at 30 seconds. The waits are 2, 3, 4.5, 6.75, 10.1, 15.2, 22.8 and then 30 seconds, so the first eight polls take about 94 seconds and the loop settles at one poll per 30 seconds. Over a full 20-minute deadline that is roughly 44 polls. The Sume docs' own examples poll every 30 seconds, and short clips usually finish faster than that, which is why the loop opens with short waits.
const auth = { Authorization: `Bearer ${Deno.env.get("SUME_API_KEY")}` };
const sub = await fetch("https://api.sume.com/v1/videos", {
method: "POST",
headers: { ...auth, "Content-Type": "application/json", "Idempotency-Key": "wan-kettle-25s-001" },
body: JSON.stringify({
model: "wan-3.0",
prompt: "Slow orbit around a ceramic kettle on a wooden table",
duration: 25,
resolution: "720p",
aspect_ratio: "16:9",
}),
});
if (sub.status !== 202) throw new Error(`submit ${sub.status}: ${await sub.text()}`);
const job = await sub.json();
const deadline = AbortSignal.timeout(20 * 60_000);
let wait = 2_000;
while (true) {
await new Promise((r) => setTimeout(r, wait));
deadline.throwIfAborted();
const res = await fetch(job.polling_url, { headers: auth, signal: AbortSignal.timeout(15_000) });
const s = await res.json();
if (s.status === "completed") {
console.log(job.id, s.unsigned_urls[0], s.usage?.cost);
break;
}
if (s.status === "failed" || s.status === "cancelled") throw new Error(JSON.stringify(s.error));
wait = Math.min(wait * 1.5, 30_000);
}The script
The loop reads status from the poll response. completed carries unsigned_urls and usage.cost; failed and cancelled carry or imply an error, and the sample throws on both. Any other status keeps the loop going.
What can go wrong
A 202 is the only success the sample accepts. A 402 means the balance does not cover the reserved amount, a 429 can be rate_limited or queue_full, and a 400 means the body was rejected; the error envelope carries a request_id you can quote to support. Poll requests are reads, which have their own per-minute budget on each key, so the loop cannot starve your submits.
On a Free workspace only six paid jobs can be accepted at once (one processing, five queued), so a second 25-second submit while the first one runs is accepted as queued rather than started. Treat queued as a normal status, not a failure.
- Keep the deadline longer than the longest job you expect, not equal to it.
- Log job.id before anything else so a crash after submit can be recovered with GET /v1/videos/{id}.
- Use usage.cost from the completed response for your books, not your own multiplication.
Sources
Related posts
More in Developers
- Deno.serve webhook receiver for Sume jobs: Web Crypto HMAC, rotation
Verify Sume job webhooks in Deno with Web Crypto: refuse an empty secret, accept any rotation entry, 300 s tolerance, return 204. Retry window is 370 s.
- Idempotency-Key from a body hash plus a take number (Node, Sume)
Build an Idempotency-Key for Sume /v1/videos from a SHA-256 of the body plus a take counter, so a retry replays and a re-roll pays. Node code.
- Sume STT returns 400 for diarize: a two-speaker fix at $0.60 an hour
Sume STT 1.0 fixes diarize and tag_audio_events server-side and rejects them with 400. For two speakers, transcribe each track and merge by word times.
- Do Sume result URLs expire? media.sume.com vs /videos content
Sume Format artifact URLs on media.sume.com do not expire and are public. The /v1/videos content route needs your key. What to store, plus a Python download.
Written by Sume