Node: stream a Sume video to disk with pipeline and a short-file check

Download a finished Sume video from /v1/videos/{id}/content in Node without loading it into memory, and fail if the file is shorter than content-length.

5 min readSume
All posts

To save a finished Sume video from Node without holding it in memory, fetch GET /v1/videos/{jobId}/content?index=0 with your bearer key, wrap res.body with Readable.fromWeb, and pipe it into createWriteStream with stream/promises pipeline. Then compare the size on disk with the content-length header and throw if they differ. The script is 18 lines and has no dependencies on Node 20 or later.

The endpoint

When a /v1/videos job is completed, the poll response lists unsigned_urls; each one points at the content endpoint of the job. The index query parameter defaults to 0 and only matters when a model returns more than one video output. The Sume docs' curl example sends the Authorization header on the download, so the script does too.

What the download step relies on (Sume docs, read 2026-10-09)
ItemValue
Content routeGET /v1/videos/{jobId}/content?index=0
Ready whenPoll status is completed
IndexDefaults to 0; use it for multi-output models
AuthAuthorization: Bearer $SUME_API_KEY
Failure shapeNon-2xx with the JSON error envelope and a request_id

Why pipeline and not arrayBuffer

response.arrayBuffer() would hold the whole file in memory. A finished clip can be large, which is fine once and a problem when 20 downloads run together. pipeline moves chunks from the network to the file with backpressure, and it rejects if either side errors, which also closes the file handle.

Readable.fromWeb converts the web ReadableStream that fetch returns into a Node stream. The script checks res.ok and res.body first, because an error body is JSON text you would otherwise write into clip.mp4.

import { createWriteStream } from "node:fs";
import { stat } from "node:fs/promises";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";

const [jobId, out = "clip.mp4"] = process.argv.slice(2);
if (!jobId) throw new Error("usage: node download.mjs <job id> [out.mp4]");

const res = await fetch(`https://api.sume.com/v1/videos/${jobId}/content?index=0`, {
  headers: { Authorization: `Bearer ${process.env.SUME_API_KEY}` },
});
if (!res.ok || !res.body) throw new Error(`content ${res.status}: ${await res.text()}`);

await pipeline(Readable.fromWeb(res.body), createWriteStream(out));

const expected = Number(res.headers.get("content-length") ?? 0);
const { size } = await stat(out);
if (expected && size !== expected) throw new Error(`short file: ${size} of ${expected} bytes`);
console.log(out, size, "bytes");

The short-file check

A connection that drops mid-download can end the stream without an error in some environments, leaving a truncated file that still has a .mp4 name. The script reads content-length, stats the file, and throws if the numbers differ. If the server sends a chunked response with no content-length, the check is skipped, so in that case verify with a probe, for example ffprobe, before you publish the file.

Keep the job id and the output path in your own records. If the download fails, run the script again for the same job.

A few operational notes. Run downloads with a small concurrency limit, because each open stream holds a connection and a file handle. Name the output after the job id so a repeated run overwrites the same file instead of creating duplicates. And do the check even when the connection was fast: a truncated file is silent, and the first sign is a player that stops early. Use a timeout on the download request itself; a stalled stream with no timeout will hold a worker until the process is killed.

  • Write to a temporary name and rename after the check passes.
  • Retry the download, not the generation.
  • Do not log the full URL if you use a signed one.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume