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.

4 min readSume
All posts

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

All Developers posts

Written by Sume