Text-to-video API in Node: submit, poll and download with a deadline
A runnable Node 18+ script that submits a text-to-video job to Sume, polls it with a deadline, and saves the MP4. No dependencies and no top-level await.

This is a complete Node.js text-to-video script for POST /v1/videos on Sume. It needs Node 18 or later for the built-in fetch, and no packages. It submits a job with an idempotency key, polls the polling URL until the status is completed or failed, and gives up at a deadline instead of looping forever.
The script
Set SUME_API_KEY, save as clip.js, and run node clip.js. The poll gap matches the docs' suggested 30 seconds. The model, prompt and 3-second Omni clip at 360p keep the first test cheap.
const fs = require("fs");
const BASE = "https://api.sume.com/v1/videos";
const headers = {
Authorization: `Bearer ${process.env.SUME_API_KEY}`,
"Content-Type": "application/json",
};
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function main() {
const body = JSON.stringify({ model: "gemini-omni-flash-1.1",
prompt: "A paper boat drifting down a rain gutter", resolution: "360p", duration: 3 });
const res = await fetch(BASE, { method: "POST", body,
headers: { ...headers, "Idempotency-Key": "node-clip-001" } });
const job = await res.json();
if (res.status !== 202) throw new Error(JSON.stringify(job));
const deadline = Date.now() + 15 * 60 * 1000;
while (Date.now() < deadline) {
await sleep(30000);
const s = await (await fetch(job.polling_url, { headers })).json();
console.log(s.status);
if (s.status === "failed") throw new Error(JSON.stringify(s.error));
if (s.status === "completed") {
const v = await fetch(`${BASE}/${job.id}/content?index=0`, { headers });
fs.writeFileSync("out.mp4", Buffer.from(await v.arrayBuffer()));
return console.log("saved out.mp4, cost", s.usage && s.usage.cost);
}
}
throw new Error("deadline reached; poll " + job.polling_url + " later");
}
main().catch((e) => { console.error(e); process.exit(1); });What each part comes from
The submit returns 202 with id, polling_url and a status of pending. Statuses are pending, in_progress, completed, failed and cancelled. The content endpoint is GET /v1/videos/{jobId}/content, and index defaults to 0. The Idempotency-Key makes a retry of the same submit return the original job, so rerunning the script after a network error does not buy a second clip; use a new key for a new clip.
Failure paths to handle
A 402 insufficient_credits means the balance could not cover the reservation. A 429 queue_full means the workspace has no accepted-job capacity left. A failed poll carries an error field. The deadline above does not cancel the job: if you stop waiting, the job can still finish and be billed, so poll its URL later rather than resubmitting.
Variations
To use a webhook instead of polling, add callback_url to the body with an HTTPS URL. Sume signs the raw JSON body and sends x-sume-webhook-timestamp and x-sume-webhook-signature headers, and the payload is Sume's standard job webhook envelope, not an OpenRouter-style event. Verify the signature before you act on it, and refuse to verify if your secret is empty.
To use another model, change model, resolution and duration together. The script above sends 3 seconds at 360p, which Gemini Omni Flash 1.1 accepts. If you pass 2 seconds to that model, the request is outside its 3 to 10 second range, so read supported_durations from GET /v1/videos/models before you change the numbers.
Before you ship anything, read the live pages again: the catalog is public, the pricing page is public, and the docs describe the request fields. A blog post is a snapshot. The catalog, the plan grid and the error table are the things that change, so write your code to read them instead of copying numbers from a page, and re-check when a new model is added.
A good habit is a small log line per submit with the model, resolution, duration, estimated cost, job id and the Idempotency-Key you used. When a job misbehaves, those six fields answer most of the questions support will ask, and they let you compare your estimate with usage.cost and the usage ledger without re-running anything.
Sources
Related posts
More in Developers
- Threads image post: 8 MB, 320 to 1440 px wide, from video frames
Threads images must be JPEG or PNG, up to 8 MB, 320 to 1440 px wide. Pull a still from a Sume clip with video-frames and clamp the long edge to 1440.
- Threads video aspect ratio up to 10:1 and 1920 px: timeline output
Threads allows video ratios from 0.01:1 to 10:1 and 1920 px, and recommends 9:16. The Sume timeline default 1080x1920 fits, and width and height are settable.
- Threads video frame rate 23 to 60 fps: conform with video-trim
Threads accepts 23 to 60 fps video. Sume video-trim output.fps takes 24, 25, 30 or 60, all inside that range. Conform a mixed-rate clip in one call.
- Threads video audio: AAC, 128 kbps, 48 kHz max, and what trim keeps
Threads wants AAC audio at up to 48 kHz and 128 kbps. Sume trim remuxes kept audio as AAC but has no bitrate field, so probe the clip and check the file.
Written by Sume