Guides · Updated 2026-10-25

Download Instagram and TikTok Videos in Node.js

This is the Node.js version of a common job: take a list of Instagram, TikTok, X or Facebook URLs and end up with MP4 files. It uses only built-in modules — fetch and streams, Node 18 or newer — and the soclip API to turn each URL into a direct media link.

Setup

  • Node.js 18+ (for global fetch and Readable.fromWeb)
  • An API key from soclip.dev (GitHub sign-in, 500 free credits, 2 credits per lookup)
  • export SOCLIP_API_KEY=sc_live_...

Two functions: resolve and download

Save this as soclip.mjs:

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

const API_KEY = process.env.SOCLIP_API_KEY;

export async function resolve(url) {
  const res = await fetch("https://api.soclip.dev/v1/media", {
    method: "POST",
    headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ url }),
  });
  const body = await res.json();
  if (!body.success) throw new Error(`${res.status}: ${body.error}`);
  return body.data;
}

export async function download(mediaUrl, path) {
  const res = await fetch(mediaUrl);
  if (!res.ok) throw new Error(`download failed: ${res.status}`);
  await pipeline(Readable.fromWeb(res.body), createWriteStream(path));
}

resolve returns { source, title, author, thumbnail, duration, medias }. medias[0] is the HD video for video posts; photo posts and carousels return image entries instead. download streams the response straight to disk, so a long video never sits in memory.

Download one video

import { resolve, download } from "./soclip.mjs";

const data = await resolve("https://www.instagram.com/reel/DcTa7nogl9z/");
const video = data.medias.find((m) => m.type === "video");
await download(video.url, "reel.mp4");
console.log("saved", data.source, data.author);

Many URLs, a few at a time

Run several downloads at once without flooding anything. This keeps four in flight:

import { readFile } from "node:fs/promises";
import { resolve, download } from "./soclip.mjs";

async function mapLimit(items, limit, fn) {
  let next = 0;
  async function worker() {
    while (next < items.length) {
      const i = next++;
      await fn(items[i], i).catch((e) => console.error("fail", items[i], e.message));
    }
  }
  await Promise.all(Array.from({ length: limit }, worker));
}

const urls = (await readFile("urls.txt", "utf8")).split("\n").map((s) => s.trim()).filter(Boolean);

await mapLimit(urls, 4, async (url, i) => {
  const data = await resolve(url);
  const video = data.medias.find((m) => m.type === "video");
  if (!video) return;
  await download(video.url, `${data.source}_${i}.mp4`);
  console.log("ok", url);
});

Errors carry the HTTP status. 400, 401 and 402 won't change on retry; 502 and 503 will often succeed a few seconds later. Failed lookups are refunded either way.

As an endpoint in your own app

Keep the API key on the server and hand the browser only what it needs:

import express from "express";
import { resolve } from "./soclip.mjs";

const app = express();
app.use(express.json());

app.post("/api/resolve", async (req, res) => {
  try {
    const data = await resolve(req.body.url);
    res.json({ title: data.title, thumbnail: data.thumbnail, medias: data.medias });
  } catch (e) {
    res.status(400).json({ error: e.message });
  }
});

app.listen(3000);

Never call the API from browser JavaScript with your key in it — anyone could copy the key from the network tab and spend your credits.

No code at all

For quick jobs there's a CLI (npm install -g soclip-cli, then soclip <url>), and for AI agents an MCP server (npx -y soclip-mcp) that works in Claude Desktop and Cursor. Setup for both is in the docs.

500 free credits on signup — 250 lookups across Instagram, TikTok, X and Facebook. Credits never expire.

Related guides