npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

youtube-transcript-nodejs

v0.1.0

Published

A complete Node.js + TypeScript port of jdepoix/youtube-transcript-api with optional self-contained PoToken generation.

Readme

youtube-transcript-nodejs

A complete Node.js + TypeScript port of jdepoix/youtube-transcript-api, with optional self-contained PoToken generation.

Node ≥ 20 License MIT Tests 182 passing Live verified

Why this port?

The Python library is the de-facto standard for YouTube transcript fetching. It works by issuing requests via the ANDROID InnerTube client, which historically is not subject to YouTube's PoToken bot-detection. Node.js has lacked a 1:1 equivalent:

| Capability | jdepoix Python | youtube-transcript | youtube-transcript-nodejs (this) | |---|---|---|---| | fetch transcript | ✅ | ✅ | ✅ | | list all tracks | ✅ | ❌ | ✅ | | translate to another language | ✅ | ❌ | ✅ | | translationLanguages enumeration | ✅ | ❌ | ✅ | | 5 formatters (SRT/VTT/JSON/Text/Pretty) | ✅ | ❌ | ✅ | | Full error hierarchy | ✅ | ⚠️ | ✅ | | Proxy / custom cookies / headers | ✅ | ❌ | ✅ | | Self-contained PoToken | ❌ | ❌ | ✅ (opt-in) | | EU CONSENT cookie handling | ❌ | ❌ | ✅ | | TypeScript-first, ESM + CJS dual | ❌ | ⚠️ | ✅ |

Install

npm install youtube-transcript-nodejs

Requires Node.js ≥ 20 (for native fetch and stable ESM).

The core API has no runtime dependencies beyond he and fast-xml-parser. Optional peer dependencies (bgutils-js, undici) are only loaded when used:

  • bgutils-js — only required for AutoPoTokenProvider
  • undici — only required when you pass proxyConfig

Quick Start

import { YouTubeTranscriptApi } from 'youtube-transcript-nodejs';

const api = new YouTubeTranscriptApi();
const transcript = await api.fetch('dQw4w9WgXcQ');
console.log(transcript.toText().content.slice(0, 200));

Test Results

End-to-end verified against the live YouTube API (test video: dQw4w9WgXcQ).

Unit tests — 182 / 182 passing

 ✓ test/unit/parsers/captionTrackParser.test.ts (12 tests)
 ✓ test/unit/config/config.test.ts                (15 tests)
 ✓ test/unit/util/util.test.ts                    (19 tests)
 ✓ test/unit/formatters/formatters.test.ts        (22 tests)
 ✓ test/unit/http/httpClient.test.ts              ( 8 tests)
 ✓ test/unit/errors/errors.test.ts                (21 tests)
 ✓ test/unit/api/innerTubeClient.test.ts          (18 tests)
 ✓ test/unit/potoken/potoken.test.ts              (11 tests)
 ✓ test/unit/parsers/watchHtmlParser.test.ts      (11 tests)
 ✓ test/unit/parsers/videoIdParser.test.ts        (12 tests)
 ✓ test/unit/api/youTubeTranscriptApi.test.ts     (11 tests)
 ✓ test/unit/http/cookieJar.test.ts               ( 7 tests)
 ✓ test/unit/parsers/xmlTranscriptParser.test.ts  ( 7 tests)
 ✓ test/unit/potoken/autoPoTokenProvider.test.ts  ( 6 tests)
 ✓ test/unit/smoke.test.ts                        ( 1 test)

Live integration tests — 4 / 4 passing

Run with RUN_LIVE=1 YT_TEST_VIDEO_ID=dQw4w9WgXcQ npm run test:live.

 ✓ live: fetch > fetches the transcript for a public video       (6.4 s)
 ✓ live: list > returns at least one transcript track            (5.9 s)
 ✓ live: translate > translates to a target language             (11.7 s)
 ✓ live: translationLanguages > returns many translation targets (6.6 s)

Build & lint

 ✓ npm run lint        — 0 errors, 0 warnings (ESLint strict)
 ✓ npm run typecheck   — tsc --noEmit (strict mode)
 ✓ npm run build       — ESM (dist/esm) + CJS (dist/cjs) dual publish
 ✓ npm pack --dry-run  — 76.4 KB tarball

Real output (sanitized)

$ npx tsx examples/basic.ts
Video: dQw4w9WgXcQ
Language: English (en)
Generated: false
Snippets: 61
---
[♪♪♪]
♪ We're no strangers to love ♪
♪ You know the rules
and so do I ♪
♪ A full commitment's
what I'm thinking of ♪
♪ You wouldn't get this
from any other guy ♪

$ npx tsx examples/list.ts
Video: dQw4w9WgXcQ
Manually created: en, de-DE, ja, pt-BR, es-419
Auto-generated:   en
Translation targets: 18
Selected: English (en, manual)
First cue: [1.36s] [♪♪♪]

$ head -10 dQw4w9WgXcQ.es.srt      # after `tsx examples/translate.ts`
1
00:00:01,360 --> 00:00:03,040
[♪♪♪]

2
00:00:18,640 --> 00:00:21,880
♪ No somos extraños al amor ♪

API Reference

new YouTubeTranscriptApi(config?)

interface YouTubeTranscriptApiConfig {
  /** HTTP proxy URL. Triggers undici dynamic import. */
  proxyConfig?: { url: string };
  /** Pre-set cookies (e.g. CONSENT for EU users). */
  cookies?: Array<{ name: string; value: string; domain?: string; path?: string }>;
  /** Default headers added to every request. */
  headers?: Record<string, string>;
  /** Replace the default NoopPoTokenProvider. */
  poTokenProvider?: PoTokenProvider;
  /** When true, ANDROID failures automatically retry via WEB + PoToken. */
  poTokenFallback?: boolean;
  /** Custom User-Agent override. */
  userAgent?: string;
  /** Custom fetch implementation (testing). */
  fetchImpl?: typeof fetch;
}

Methods

api.fetch(videoId, opts?): Promise<FetchedTranscript>

const t = await api.fetch('dQw4w9WgXcQ', {
  languages: ['en', 'en-US', 'en-GB'],   // ordered preference
  preserveFormatting: false,              // keep <i>/<b> tags
});

api.list(videoId): Promise<TranscriptList>

const list = await api.list('dQw4w9WgXcQ');
console.log('manual:', list.manuallyCreatedTranscripts.map(t => t.languageCode));
console.log('auto:', list.generatedTranscripts.map(t => t.languageCode));

const en = list.findTranscript(['en', 'en-US']);
const data = await en.fetch();

api.translate(videoId, targetLang, opts?): Promise<FetchedTranscript>

const zh = await api.translate('dQw4w9WgXcQ', 'zh-Hans');
console.log(zh.toSRT().content);

api.translationLanguages(videoId, sourceLang): Promise<TranslationLanguage[]>

const langs = await api.translationLanguages('dQw4w9WgXcQ', 'en');
// [{ languageCode: 'es', languageName: 'Spanish' }, ...]

api.setPoTokenProvider(provider): void

Swap the provider at runtime.

Formatters

FetchedTranscript exposes five convenience methods plus a generic format():

t.toRaw();     // JSON array of { text, start, duration }
t.toText();    // Plain text, one snippet per line
t.toSRT();     // SRT subtitle format
t.toVTT();     // WebVTT subtitle format
t.toPretty();  // [HH:MM:SS.mmm] text

// Or bring your own:
import type { Formatter } from 'youtube-transcript-nodejs';
class MyCsvFormatter implements Formatter { /* ... */ }
t.format(new MyCsvFormatter());

Errors

The package throws specific error subclasses that all extend YouTubeTranscriptError:

| Class | Trigger | |---|---| | InvalidVideoId | Provided id cannot be parsed (not 11 chars, not a URL). | | FailedToCreateConsentCookie | EU consent gate can't be auto-handled. | | VideoUnavailable | InnerTube playabilityStatus.status === ERROR + "This video is unavailable". | | VideoUnplayable | Other unplayable status, with reason and subreason populated. | | TooManyRequests | Explicit rate-limit (not normally thrown; 429 maps to IpBlocked). | | IpBlocked | HTTP 429 or captcha gate; status code attached. | | BotDetected | InnerTube reports BotGuard block (bent-quote reason). | | PoTokenRequired | Caption track's baseUrl contains &exp=xpe. | | TranscriptsDisabled | InnerTube returns no caption tracks. | | NotTranslatable | .translate() on a non-translatable track. | | TranslationLanguageNotAvailable | Target language not in translationLanguages. | | NoTranscriptFound | None of the requested language codes match. | | YouTubeRequestFailed | Generic HTTP failure. | | YouTubeDataUnparsable | Watch HTML / InnerTube JSON couldn't be parsed. |

import { NoTranscriptFound, BotDetected } from 'youtube-transcript-nodejs';

try {
  await api.fetch(videoId);
} catch (err) {
  if (err instanceof BotDetected) {
    // Provide a PoToken provider or proxy
  } else if (err instanceof NoTranscriptFound) {
    console.log('Try different languages:', err.requestedLanguageCodes);
  } else {
    throw err;
  }
}

Proxy, Cookies, Headers

const api = new YouTubeTranscriptApi({
  proxyConfig: { url: 'http://user:[email protected]:8080' },
  // or: 'socks5://localhost:1080'
  cookies: [
    { name: 'CONSENT', value: 'YES+cb', domain: '.youtube.com' },
  ],
  headers: { 'X-Forwarded-For': '1.2.3.4' },
  userAgent: 'MyCrawler/1.0',
});

PoToken Support

YouTube may rate-limit or block ANDROID InnerTube on some networks. To enable automatic retry via the WEB client + PoToken:

import { YouTubeTranscriptApi, AutoPoTokenProvider } from 'youtube-transcript-nodejs';

// 1. Self-contained (requires `npm install bgutils-js`)
const api = new YouTubeTranscriptApi({
  poTokenProvider: new AutoPoTokenProvider(),
  poTokenFallback: true,
});

// 2. External HTTP provider (e.g. bgutil-ytdlp-pot-provider sidecar)
import { HttpPoTokenProvider } from 'youtube-transcript-nodejs';
const api = new YouTubeTranscriptApi({
  poTokenProvider: new HttpPoTokenProvider({
    baseUrl: 'http://localhost:4416',
  }),
  poTokenFallback: true,
});

// 3. Custom — implement PoTokenProvider yourself
const myProvider: PoTokenProvider = {
  async generateForVideo(videoId) {
    return {
      poToken: '...',
      visitorData: '...',
      expiresAt: new Date(Date.now() + 12 * 3600 * 1000),
    };
  },
};

AutoPoTokenProvider lazy-loads bgutils-js at runtime. If the package is not installed, generateForVideo() throws BotDetected so the caller can surface a clear error.

Differences from the Python Library

| | Python | Node (this package) | |---|---|---| | HTTP client | requests | Native fetch (Node 18+) | | InnerTube key extraction | regex "INNERTUBE_API_KEY":\s*"([^"]+)" | Same regex + 2 fallbacks | | Default User-Agent | python-requests/X.Y.Z | Mozilla/5.0 ... Chrome/124 (PoToken-friendly) | | XML parser | defusedxml.ElementTree | fast-xml-parser for root + regex for <text> (preserves inline HTML) | | Formatters | Module-level functions | Classes implementing Formatter interface | | Cookies | requests.Session jar | Custom minimal CookieJar | | Retry on 429 | urllib3.Retry | Exponential backoff (configurable) | | PoToken | Passive detection only | Pluggable provider + self-contained default |

Limitations

  • Not every video has subtitles; TranscriptsDisabled is expected behavior.
  • YouTube may change its protocol at any time; pin a version.
  • AutoPoTokenProvider depends on bgutils-js staying current with YouTube's botguard obfuscation.
  • Live (streaming) videos are not supported.
  • Region-locked videos require a proxy / cookie in the right region.
  • ASR (auto-generated) captions can be inaccurate.

Contributing

git clone <repo>
cd yttapi
npm install
npm run lint       # ESLint
npm run typecheck  # tsc --noEmit
npm test           # unit tests (always runs, 182 tests)
npm run test:live  # live integration tests (RUN_LIVE=1, 4 tests)
npm run build      # ESM + CJS dual publish

Live tests default to skipped. To exercise them:

RUN_LIVE=1 YT_TEST_VIDEO_ID=dQw4w9WgXcQ npm run test:live

License

MIT