Node SDK
The official, typed Node.js / TypeScript client. Fetch transcripts, channels, playlists, and search as structured data, ESM + CommonJS, zero runtime dependencies.
Official SDK. Source + issues on GitHub. Your key falls back to the TRANSCRIPTFETCH_API_KEY env var, keep it server-side. One credit per successful caption fetch (AI Fallback Transcription, on by default for videos without captions, bills per started 5 minutes of audio); failed/blocked/no-transcript requests are free.
Using n8n? There is a community node that wraps this API for n8n workflows, no code required: set up the n8n node.
Install
npm install transcriptfetch@2.4.0SDK 2.4.0 requires Node 18+ (uses the built-in fetch). Use a currently supported Node.js release for a new project. Ships types for TypeScript.
Quickstart
Get an API key (50 free credits a month) at the dashboard, then:
import { TranscriptFetch } from "transcriptfetch"; // apiKey falls back to the TRANSCRIPTFETCH_API_KEY env var const tf = new TranscriptFetch({ apiKey: "tf_live_..." }); const t = await tf.transcripts.video("https://youtu.be/aircAruvnKk"); if (t.status === "processing") { console.log("Still transcribing; poll this job:", t.jobId); } else { console.log(t.title); for (const seg of t.segments) { console.log(`[${seg.start.toFixed(1)}] ${seg.text}`); } } console.log("credits left:", t.usage?.balance);
The single-video HTTP response returns timestamped segments by default and omits text. The SDK normalizes that missing text to null; join the segment text if you need both forms locally. timestamps: false requests plain text through the SDK:
const transcript = await tf.transcripts.video("https://youtu.be/aircAruvnKk", { mode: "captions", timestamps: false, }); // Captions mode returns a finished transcript or throws; it never starts a job. console.log(transcript.text);
Endpoints
video/channel/playlist accept URLs or raw IDs (normalized automatically).
await tf.transcripts.video(video); // single transcript await tf.transcripts.channel(channel, { limit, cursor }); // a channel's videos (metadata) await tf.transcripts.playlist(playlist, { limit, cursor }); // a playlist's videos await tf.transcripts.search(query, { limit, cursor }); // search YouTube await tf.transcripts.batch(videoIds); // up to your plan’s batch limit await tf.transcripts.batch(videoIds, { mode: "captions" }); // captions only - no audio fallback // captionless entries come back outcome "processing" with a jobId: re-send the // batch later, or poll tf.transcripts.job(jobId) await tf.health(); // unauthenticated liveness probe
SDK 2.4.0 adds mode, timestamps, and callbackUrl options to video(), plus the tf.monitors resource. Channel since_video_id and YouTube listing filters still use request() with the API’s snake_case field names. The generic method retains retries, idempotency keys, and typed errors. Batch limits are 50 on free/Basic/Pro and 500 on Mega/Scale.
For example:
const { data } = await tf.request("POST", "/api/v2/transcripts/channel", { body: { channel: "@lexfridman", tab: "shorts", limit: 10 }, idempotent: true, });
Monitors
SDK 2.4.0 supports the full monitor API. Creation starts scheduled checks for new videos. Every check, the initial baseline included, costs 1 credit; optional transcripts cost extra.
const client = new TranscriptFetch({ timeout: 120_000 }); const monitor = await client.monitors.create("@lexfridman", { intervalMinutes: 60, transcripts: true, }); // Save monitor.webhookSecret securely: it is only returned on creation. const current = await client.monitors.get(monitor.id); const all = await client.monitors.list({ since: current.lastEventId ?? undefined }); for await (const event of client.monitors.iterEvents(monitor.id, { limit: 10 })) { console.log(event.type, event.data); } await client.monitors.update(monitor.id, { status: "paused" }); // await client.monitors.check(monitor.id) checks now and may spend credits. // await client.monitors.delete(monitor.id) removes the monitor and its events.
Pass webhookUrl to create() for push delivery, or use events() / iterEvents() to read events. update(id, { webhookUrl: null }) removes a webhook; omitted settings stay unchanged. check() can return error on a listing failure even with HTTP 200. Options and results use camelCase, including intervalMinutes, nextCursor and hasNew. See the monitor guide for signature verification and billing.
Long media and jobs
AI Fallback Transcription can return 202 with a job instead of a finished transcript. The SDK exposes status: "processing" and jobId; poll tf.transcripts.job(jobId) until status is "completed" or "failed". A failed job is a returned result, so check its status and error. Short media may finish inline. To request a transcription webhook, pass callbackUrl to video(). The full contract, including what AI Fallback Transcription costs, is in the transcription guide.
Pagination
List endpoints are cursor-paginated. Iterate every result with an async generator:
// Iterate every result without managing cursors for await (const video of tf.transcripts.iterChannel("@lexfridman", { limit: 10 })) { console.log(video.videoId, video.title); } // ...or page manually via page.nextCursor and the cursor option.
Errors
All errors subclass TranscriptFetchError. API errors carry .status, .code, .message, and .requestId.
import { InsufficientCreditsError, RateLimitError, APIError, } from "transcriptfetch"; try { await tf.transcripts.video("bad"); } catch (err) { if (err instanceof InsufficientCreditsError) { // 402: top up at /pricing } else if (err instanceof RateLimitError) { console.log(err.retryAfter); // 429 } else if (err instanceof APIError) { console.log(err.status, err.code, err.requestId); } }
Reliability built in. Automatic retries on 429 (honoring Retry-After) and 5xx with exponential backoff; every write auto-sends an Idempotency-Key so a retried request is never double-charged. Configure via new TranscriptFetch({ apiKey, baseUrl, timeout: 30000, maxRetries: 2 }).
Prefer raw HTTP? The API is plain HTTPS/JSON, see the endpoint reference for every route and the response format.