Guides · YouTube Transcript Scraper · Published July 15, 2026

How to get YouTube transcripts in Node.js: apify-client with real error handling

Fetch YouTube transcripts in Node.js with the official apify-client SDK: batch video IDs in, time-coded JSON out, and typed success:false rows for captionless videos instead of thrown exceptions. Full code, costs, and when the free library is the better choice.

By slvDev · Updated and technically verified July 15, 2026


The short answer: for an unattended Node.js pipeline, one production-oriented route to YouTube transcripts is the official apify-client SDK calling the YouTube Transcript Scraper — batch up to 50 video IDs per call, get time-coded JSON back, and handle failures as data instead of exceptions ($3.00 per 1,000 delivered transcripts; a video that can't deliver returns a typed success: false row and costs $0). For notebooks and low-volume scripts from a residential IP, the free Python library remains the right tool — the trade-offs are the same ones covered in the Python guide, and JavaScript's open-source options inherit the same datacenter IP blocks.

Setup: one package, one token

npm install apify-client

The token comes from Apify Console → Settings → API tokens. The free tier works, and billing runs per delivered result on your existing account — no YouTube Data API key, and none of your Google quota is involved.

Fetch a batch, branch on success

import { ApifyClient } from "apify-client";

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });

const run = await client
  .actor("apihq/youtube-transcript-scraper")
  .call({
    videoIds: ["jNQXAC9IVRw", "dQw4w9WgXcQ", "00000000000"],
    language: "en",
    metadata: true,
  });

const { items } = await client.dataset(run.defaultDatasetId).listItems();

const transcripts = [];
const misses = [];

for (const item of items) {
  if (item.success) {
    transcripts.push(item);            // billed: $0.003
  } else {
    misses.push(item);                 // free, machine-readable
    console.warn(item.video_id, "->", item.code);
    // e.g. "00000000000 -> PLAYABILITY"
  }
}

console.log(`${transcripts.length} transcripts, ${misses.length} misses`);

The part that matters for anything unattended: the bad ID in that batch does not throw. It arrives as a row with a stable code (NO_CAPTIONS, PLAYABILITY, DEADLINE_EXCEEDED — the full set is in the error-code registry) and a request_id for support. Your catch block is reserved for real infrastructure failures, not for content that happens to lack captions.

From segments to plain text

Each transcript arrives as time-coded segments. For search indexes, embeddings, or summaries you usually want one string:

const docs = transcripts.map((t) => ({
  videoId: t.video_id,
  title: t.metadata?.title,
  text: t.transcript.map((s) => s.text).join(" "),
}));

Scaling up: a whole channel

The transcript actor takes IDs, so for a channel you chain the YouTube Channel Scraper ($0.50 per 1,000 videos) in front and feed the IDs through in batches of 50:

const listing = await client
  .actor("apihq/youtube-channel-scraper")
  .call({ channelUrl: "@mkbhd", maxResults: 200 });

const { items: videos } = await client
  .dataset(listing.defaultDatasetId)
  .listItems();

const ids = videos.filter((v) => v.success).map((v) => v.video_id);

const corpus = [];
for (let i = 0; i < ids.length; i += 50) {
  const batch = ids.slice(i, i + 50);
  const r = await client
    .actor("apihq/youtube-transcript-scraper")
    .call({ videoIds: batch, language: "en" });
  const { items } = await client.dataset(r.defaultDatasetId).listItems();
  corpus.push(...items.filter((t) => t.success));
}

console.log(`Corpus: ${corpus.length} documents`);

Cost check for a 200-video channel where 180 videos have captions: $0.10 for the listing plus $0.54 for the transcripts — about $0.64 for a channel-sized corpus, with the 20 captionless videos costing nothing.

When not to use this

  • A few hundred videos from your laptop: the free Python library from a residential IP costs nothing — use it and skip the SDK. (Deployed to a datacenter it hits YouTube's IP blocks — that failure mode explained.)
  • Videos with no captions at all: nothing caption-based can help; you'd need speech-to-text on the audio.
  • Private, members-only, or age-gated videos are out of reach for every method that doesn't own the video.

Deploying this?

Run one input on the YouTube Transcript Scraper. It comes back as either a billed transcript row or an unbilled, typed failure row — the contract this guide builds on.

A one-item first run costs under a cent, on your existing Apify account.