HomeBlog

YouTube Transcript in Node.js: Native fetch, No npm Install

Node.js code retrieving a YouTube transcript with the built-in fetch API

You don't need a package for this. Fetching a YouTube transcript in Node.js is one HTTPS call, fetch has been global since Node 18, and the endpoint speaks plain JSON. Everything in this post runs in an empty directory with no package.json — by the end you'll have a transcribe() function that returns title, publish date, and full transcript text for any YouTube URL, with error handling that doesn't guess.

The npm registry has a dozen transcript scrapers, and they share a fate: they fetch from YouTube using your server's IP, and YouTube blocks datacenter IPs. A hosted API sidesteps that entirely.

The whole thing

const KEY = process.env.YT2TEXT_KEY;

async function transcribe(videoUrl, maxChars = 150000) {
  const qs = new URLSearchParams({ url: videoUrl, maxChars: String(maxChars) });
  const res = await fetch(`https://youtube2text.org/api/transcribe?${qs}`, {
    headers: { "x-api-key": KEY },
  });
  const body = await res.json();
  if (!res.ok) {
    const err = new Error(`${body.error.code}: ${body.error.message}`);
    err.code = body.error.code;
    err.retryAfterSeconds = body.error.retryAfterSeconds;
    throw err;
  }
  return body.result;
}

const t = await transcribe("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
console.log(`${t.title} — ${t.contentSize} chars`);
console.log(t.content.slice(0, 300));

Save as index.mjs (top-level await wants ESM), run node index.mjs. The url parameter takes any YouTube URL variant or a bare 11-character video id, and maxChars caps the transcript length — 150000 is both the default and the ceiling, so trim it down when the text is going into an LLM prompt.

No key yet? Grab the demo one:

curl https://youtube2text.org/api/demo-key

It returns {"success":true,"apiKey":"yt_..."}. Be warned that it's shared across all anonymous users and limited to 5 videos per month per IP — a tire-kicking key, not a production one.

Errors are JSON, so treat them that way

Every failure has a stable code field. The ones worth branching on:

try {
  await transcribe("https://youtu.be/xxxxxxxxxxx");
} catch (err) {
  switch (err.code) {
    case "RATE_LIMIT_EXCEEDED":
      console.log(`Back off for ${err.retryAfterSeconds}s, then retry`);
      break;
    case "TRANSCRIPT_UNAVAILABLE":
      console.log("Video has no captions — nothing to fetch, skip it");
      break;
    case "VIDEO_NOT_FOUND":
    case "INVALID_URL":
      console.log("Bad input, fix the URL");
      break;
    default:
      throw err; // UNAUTHORIZED, YOUTUBE_ERROR — genuinely unexpected
  }
}

TRANSCRIPT_UNAVAILABLE is the honest limitation of the whole approach: transcripts come from YouTube's own caption tracks, manual or auto-generated, English preferred with fallback to whatever language exists. A video with captions disabled returns a 404 and there's nothing to retry. Handle it as a normal case, not an exception path.

TypeScript, if that's your thing

The response shape is stable, so a couple of interfaces get you full type safety with zero dependencies:

interface TranscriptResult {
  videoId: string;
  title: string;
  pubDate: string;
  content: string;
  contentSize: number;
  truncated: boolean;
}

interface ApiError {
  code: string;
  message: string;
  status: number;
  retryAfterSeconds?: number;
}

Type body as { result: TranscriptResult } | { error: ApiError } and narrow on res.ok. No any, no casting.

What this plugs into

Most people calling this from Node are feeding transcripts to a language model or a content pipeline — turning talks into drafts is covered in YouTube video to blog post, and if your agent framework speaks MCP you can skip the HTTP layer entirely with the MCP server for Claude and Codex. The same endpoint works identically from Python if your stack is mixed — see the Python version.

The full parameter and error reference lives at youtube2text.org/api.md. It's written for machines to read, which as a side effect makes it mercifully short for humans too.