Node script for a 9:16 TikTok video: check the model, then submit

A Node 20 fetch script that confirms a Sume model lists 9:16 and your duration, submits one 12-second 720p job, and saves an MP4 that fits TikTok's API limits.

5 min readSume
All posts

To make a 9:16 TikTok video from Node, ask GET /v1/videos/models whether your chosen model lists 9:16 and your duration, then POST /v1/videos, poll the job, and save the file. The script below does that with Node 20's built-in fetch and no packages, and it fails early with a clear message instead of burning a paid job on an unsupported request.

The targets come from TikTok's Media Transfer Guide (read 2026-10-10) for the Content Posting API: MP4 is recommended, with WebM and MOV also accepted; H.264 is the recommended codec; frame rate must be between 23 and 60 FPS; each side must be between 360 and 4096 pixels; the file can be up to 4 GB. On length it says all TikTok creators can post 3-minute videos, while some can post 5 or 10 minutes, and the API handles videos up to 10 minutes.

The script

Save it as make.mjs (the .mjs extension lets Node run the top-level await) and run node make.mjs with SUME_API_KEY set in the environment. It targets Seedance 2.5, which the Sume docs list at 4 to 30 seconds and 480p/720p/1080p.

import { writeFile } from "node:fs/promises";
const API = "https://api.sume.com";
const headers = {
  Authorization: `Bearer ${process.env.SUME_API_KEY}`,
  "Content-Type": "application/json",
};
async function call(path, init = {}) {
  const res = await fetch(API + path, { headers, ...init });
  if (!res.ok) throw new Error(`${path} -> ${res.status} ${await res.text()}`);
  return res;
}
const rel = (u) => { const x = new URL(u); return x.pathname + x.search; };
const { data } = await (await call("/v1/videos/models")).json();
const m = data.find((x) => x.id === "seedance-2.5");
if (!m?.supported_aspect_ratios.includes("9:16") || !m.supported_durations.includes(12))
  throw new Error("model lacks 9:16 or 12 s");
const body = { model: m.id, aspect_ratio: "9:16", resolution: "720p",
  duration: 12, prompt: "Hands unwrap a ceramic mug on a wooden desk, natural light" };
let job = await (await call("/v1/videos", { method: "POST",
  headers: { ...headers, "Idempotency-Key": "tiktok-mug-001" },
  body: JSON.stringify(body) })).json();
while (!["completed", "failed", "cancelled"].includes(job.status)) {
  await new Promise((r) => setTimeout(r, 15000));
  job = await (await call(rel(job.polling_url))).json();
}
if (job.status !== "completed") throw new Error(job.error ?? job.status);
const file = await call(rel(job.unsigned_urls[0]));
await writeFile("tiktok-mug.mp4", Buffer.from(await file.arrayBuffer()));
console.log("saved, cost", job.usage?.cost);

What the preflight buys you

Limits differ by model. The Video Generation page tells you to read the models endpoint before submitting, because supported resolutions, aspect ratios and durations are per model. The script reads exactly those fields (supported_aspect_ratios, supported_durations), so a change in the catalog stops the run before any money is reserved.

The preflight also keeps the spend predictable. Per the same page, Sume reserves the provider list price times 1.25 when you submit, and the finished job reports usage.cost as the billable amount, which the script prints. Run a 4-second job first if you are testing the plumbing; the model's range starts at 4 seconds, so that is the shortest valid request. Change the 12 in both the check and the body together.

The submit uses an Idempotency-Key, which the docs say makes retries safe: a replay returns the original job. That matters in a loop that might restart after a network drop.

TikTok Media Transfer Guide limits against the script's request (read 2026-10-10)
TikTok limitScript requestFits?
MP4 recommendedSume returns the file at the content URLCheck the container with a probe
23 to 60 FPSNot set by the requestProbe it; the Sume docs give no frame rate
360 to 4096 px per side720p at 9:16Yes, nominally 720 by 1280
Up to 4 GBOne 12-second clipFar below the cap
3 minutes for all creators12 secondsYes

Where the guarantees stop

Two rows in that table are not proven by the request. The Sume pages describe the model's resolution tier and duration range, not the frame rate or the exact pixel size of the generated file, and TikTok enforces both on upload. Probe the saved MP4 with a tool you trust, or with Sume's Video inspect once the file is imported, before it goes to the Content Posting API.

Also remember that the 12-second request is a building block. If you want a longer TikTok, the same preflight pattern works for each clip: loop over a shot list, check each, submit with distinct idempotency keys, and join the results afterwards.

Common failures

A 400 on the submit usually means the body carries a field Sume rejects on this route. The docs state that size, seed and a non-empty provider.options return an error on v1, so keep the body to model, prompt, aspect_ratio, resolution and duration. A failed job carries an error field, which the script throws. A job that stays pending is not an error: the docs say generation can take several minutes depending on the model, resolution and load, so keep polling at a moderate interval.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume