YouTube Transcript API for JavaScript and TypeScript

By TranscriptAPI TeamPublished June 25, 202612 min read

The fastest safe way to fetch a YouTube transcript in JavaScript

If you need a YouTube transcript API in JavaScript, the practical production pattern is simple: call https://transcriptapi.com/api/v2/youtube/transcript from trusted server-side JavaScript, pass the YouTube URL in video_url, and send your API key in the Authorization: Bearer <key> header.

This guide covers Node 20+ native fetch, axios, async/await, TypeScript response types, retry behavior, and a browser-safe route pattern. The examples use the default JSON transcript response shape: video_id, language, and transcript, where each transcript segment has text, start, and duration.

If you already have transcript segments and want timestamp filtering, CSV export, or time-range slicing, use the focused YouTube transcript timestamps in Node.js guide. This article stays on the broader JavaScript and TypeScript API integration.

Before you copy code: keep the bearer key server-side

A browser can run fetch, but public browser code must not contain your TranscriptAPI bearer key. Treat the key like any production secret: read it from an environment variable on a backend route, worker, job runner, or API route, then have the browser call your own route.

Use Authorization: Bearer <key> for TranscriptAPI. Do not send X-API-Key for this endpoint. In this article, the only browser snippet calls /api/transcript on your own app and never calls TranscriptAPI directly.

Quick start with the TranscriptAPI endpoint

Endpoint, auth, and request parameters

Source of truth: TranscriptAPI API docs.

For a single transcript request, use these pieces:

  • Endpoint: https://transcriptapi.com/api/v2/youtube/transcript
  • Method: GET
  • Auth header: Authorization: Bearer <key>
  • Required query parameter: video_url
  • Useful optional parameters: format=json, format=text, include_timestamp=true, and send_metadata=true

The TypeScript interfaces below target the default structured JSON shape with timestamps. If you request format=text or turn timestamps off, type that as a separate variant instead of mixing response shapes in the same interface.

Credit behavior to plan around

TranscriptAPI credits are based on successful requests. Failed 4xx, 5xx, and 429 responses cost 0 credits. That is useful for batch jobs: you can log a missing transcript, retry a transient 5xx, or back off on 429 without paying for failed calls. Still, you should cap retries and log safe fields only.

JavaScript example with native fetch and async/await

Minimal Node 20+ fetch example

Save this as get-transcript.mjs and run it with Node 20 or newer. It reads the key from TRANSCRIPTAPI_KEY, builds the URL with URL, checks response.ok before parsing JSON, and verifies the response shape.

const endpoint = "https://transcriptapi.com/api/v2/youtube/transcript";
const videoUrl = process.argv[2] ?? "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
const apiKey = process.env.TRANSCRIPTAPI_KEY;

if (!apiKey) {
  throw new Error("Missing TRANSCRIPTAPI_KEY environment variable");
}

function assertTranscriptResponse(data) {
  if (!data || typeof data !== "object") throw new Error("Response is not an object");
  if (typeof data.video_id !== "string") throw new Error("Missing video_id");
  if (typeof data.language !== "string") throw new Error("Missing language");
  if (!Array.isArray(data.transcript)) throw new Error("Missing transcript array");

  const first = data.transcript[0];
  if (!first || typeof first.text !== "string") throw new Error("Missing segment text");
  if (typeof first.start !== "number") throw new Error("Missing segment start");
  if (typeof first.duration !== "number") throw new Error("Missing segment duration");
}

async function getTranscript(videoUrl) {
  const url = new URL(endpoint);
  url.searchParams.set("video_url", videoUrl);
  url.searchParams.set("format", "json");
  url.searchParams.set("include_timestamp", "true");

  const response = await fetch(url, {
    headers: {
      Authorization: `Bearer ${apiKey}`
    }
  });

  if (!response.ok) {
    const body = await response.text();
    throw new Error(`TranscriptAPI request failed with ${response.status}: ${body.slice(0, 160)}`);
  }

  const data = await response.json();
  assertTranscriptResponse(data);
  return data;
}

const data = await getTranscript(videoUrl);
console.log({
  video_id: data.video_id,
  language: data.language,
  transcript_segments: data.transcript.length
});

Run it like this:

export TRANSCRIPTAPI_KEY="your_key_here"
node get-transcript.mjs "https://www.youtube.com/watch?v=dQw4w9WgXcQ"

Node's native fetch is enough for most server-side JavaScript jobs. Use it for workers, queues, scripts, API routes, and backend services where you do not need a custom HTTP client.

Convert transcript segments to plain text

The JSON response gives you segments. If your summarizer or search index wants a single text string, join the text fields. Keep timestamp transforms, CSV, and time slicing in the dedicated Node timestamps guide.

function transcriptToText(transcript) {
  return transcript
    .map((segment) => segment.text.trim())
    .filter(Boolean)
    .join(" ");
}

const plainText = transcriptToText(data.transcript);
console.log(plainText.slice(0, 500));

JavaScript example with axios

Install axios and call the same endpoint

If your codebase standardizes on axios, use the same endpoint, query parameters, and bearer auth header.

npm install axios
import axios from "axios";

const endpoint = "https://transcriptapi.com/api/v2/youtube/transcript";
const apiKey = process.env.TRANSCRIPTAPI_KEY;
const videoUrl = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";

if (!apiKey) {
  throw new Error("Missing TRANSCRIPTAPI_KEY environment variable");
}

const response = await axios.get(endpoint, {
  params: {
    video_url: videoUrl,
    format: "json",
    include_timestamp: "true"
  },
  headers: {
    Authorization: `Bearer ${apiKey}`
  },
  timeout: 30000
});

console.log({
  status: response.status,
  video_id: response.data.video_id,
  language: response.data.language,
  transcript_segments: response.data.transcript.length
});

Axios is convenient when your team already uses interceptors, request defaults, timeouts, or shared error handling. The TranscriptAPI-specific pieces do not change.

Handling AxiosError cleanly

Axios rejects non-2xx responses by default. On an API response error, inspect error.response.status, error.response.headers, and error.response.data. Do not log error.config unless your logger redacts headers. If your app uses a structured logger, configure that logger to redact Authorization before logs leave the process.

import axios from "axios";

function summarizeAxiosError(error) {
  if (axios.isAxiosError(error) && error.response) {
    const status = error.response.status;
    const headers = error.response.headers ?? {};
    const retryAfter = headers["retry-after"] ?? headers["Retry-After"];

    return {
      hasResponse: true,
      status,
      retryAfter: retryAfter ? String(retryAfter) : undefined,
      retryable: status === 408 || status === 429 || status === 503 || status >= 500,
      body: error.response.data
    };
  }

  if (axios.isAxiosError(error) && error.request) {
    return { hasResponse: false, retryable: true, code: error.code ?? "network_error" };
  }

  return { hasResponse: false, retryable: false, code: "unexpected_error" };
}

Add TypeScript types for the response

Transcript segment and response interfaces

Use the actual default JSON response shape. Do not add metadata fields to the main response interface unless your request enables and verifies those fields separately.

export interface TranscriptSegment {
  text: string;
  start: number;
  duration: number;
}

export interface TranscriptResponse {
  video_id: string;
  language: string;
  transcript: TranscriptSegment[];
}

If you request format=text, keep it separate because transcript is a string in that response mode. If you request metadata with send_metadata=true, model that as a separate verified response variant for that request path.

Typed helper with a discriminated result

This helper returns { ok: true, data } for a successful transcript and a typed failure for expected API outcomes. It throws only for fatal local configuration problems, such as a missing environment variable.

const ENDPOINT = "https://transcriptapi.com/api/v2/youtube/transcript";

type TranscriptErrorReason =
  | "auth"
  | "forbidden"
  | "no_transcript"
  | "rate_limited"
  | "server_error"
  | "bad_request"
  | "billing"
  | "network"
  | "unknown";

export type TranscriptResult =
  | { ok: true; data: TranscriptResponse }
  | {
      ok: false;
      status: number;
      reason: TranscriptErrorReason;
      retryAfterMs?: number;
      detail?: unknown;
    };

function reasonForStatus(status: number): TranscriptErrorReason {
  if (status === 401) return "auth";
  if (status === 403) return "forbidden";
  if (status === 402) return "billing";
  if (status === 404) return "no_transcript";
  if (status === 429) return "rate_limited";
  if (status === 400 || status === 422) return "bad_request";
  if (status >= 500 || status === 408) return "server_error";
  return "unknown";
}

function getRetryAfterMs(headers: Headers): number | undefined {
  const value = headers.get("retry-after");
  if (!value) return undefined;

  const seconds = Number(value);
  if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;

  const dateMs = Date.parse(value);
  return Number.isFinite(dateMs) ? Math.max(0, dateMs - Date.now()) : undefined;
}

function shouldRetry(status: number): boolean {
  return status === 408 || status === 429 || status === 503 || status >= 500;
}

function assertTranscriptResponse(data: unknown): asserts data is TranscriptResponse {
  const value = data as Partial<TranscriptResponse>;
  if (!value || typeof value.video_id !== "string") throw new Error("Missing video_id");
  if (typeof value.language !== "string") throw new Error("Missing language");
  if (!Array.isArray(value.transcript)) throw new Error("Missing transcript array");

  for (const segment of value.transcript) {
    if (typeof segment.text !== "string") throw new Error("Missing segment text");
    if (typeof segment.start !== "number") throw new Error("Missing segment start");
    if (typeof segment.duration !== "number") throw new Error("Missing segment duration");
  }
}

async function sleep(ms: number): Promise<void> {
  await new Promise((resolve) => setTimeout(resolve, ms));
}

export async function getTranscript(videoUrl: string, maxRetries = 2): Promise<TranscriptResult> {
  const apiKey = process.env.TRANSCRIPTAPI_KEY;
  if (!apiKey) throw new Error("Missing TRANSCRIPTAPI_KEY environment variable");

  for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
    const url = new URL(ENDPOINT);
    url.searchParams.set("video_url", videoUrl);
    url.searchParams.set("format", "json");
    url.searchParams.set("include_timestamp", "true");

    let response: Response;
    try {
      response = await fetch(url, {
        headers: { Authorization: `Bearer ${apiKey}` }
      });
    } catch (error) {
      if (attempt === maxRetries) return { ok: false, status: 0, reason: "network", detail: error };
      await sleep(500 * 2 ** attempt);
      continue;
    }

    if (response.ok) {
      const data: unknown = await response.json();
      assertTranscriptResponse(data);
      return { ok: true, data };
    }

    const retryAfterMs = getRetryAfterMs(response.headers);
    if (shouldRetry(response.status) && attempt < maxRetries) {
      await sleep(retryAfterMs ?? Math.min(5000, 500 * 2 ** attempt));
      continue;
    }

    return {
      ok: false,
      status: response.status,
      reason: reasonForStatus(response.status),
      retryAfterMs,
      detail: await response.text()
    };
  }

  return { ok: false, status: 0, reason: "unknown" };
}

Compile this with strict TypeScript settings. For a minimal local setup:

npm install -D typescript @types/node
npx tsc --noEmit

Error handling for production jobs

401 and 403 auth errors

Treat 401 and 403 as non-retryable. Check whether TRANSCRIPTAPI_KEY is missing, expired, copied from the wrong environment, or attached to an account state that cannot access the endpoint. Do not log the key. Log a redacted auth state such as auth_header: "[REDACTED]".

404 or no transcript available

Treat a missing transcript as a data outcome, not a process crash. For batch jobs, return a typed result such as { ok: false, reason: "no_transcript" }, record the video ID or a hash of the URL, and move on.

Common causes include a private or deleted video, unavailable captions, a very new upload, or a video whose captions are not available in the requested language or region.

429 rate limiting

On 429, read Retry-After if present. If it is absent, use capped exponential backoff and a small maximum retry count. Failed 429 calls cost 0 credits, but uncontrolled retries can still slow your queue or cause noisy logs.

5xx and network errors

Retry short transient 5xx and network failures with capped backoff. Log safe fields: status code, attempt count, video ID or URL hash, and an internal error code. If the error persists, stop retrying and surface the failure to your job monitor or support workflow. Failed 5xx calls cost 0 credits.

400, 402, and 422 quick notes

A 400 or 422 usually means the request or video URL needs to be fixed before retrying. A 402 points to account or billing state, not a JavaScript bug. Failed 4xx calls cost 0 credits, but most 4xx statuses should not be retried until something changes.

For broader troubleshooting, see the guides to youtube-transcript-api production errors, YouTube transcript API not working, and YouTube API quota exceeded transcript options.

Use the API from a browser app without exposing your key

Minimal Express backend route pattern

This is the production-safe pattern for a browser app: the browser calls your route, and your route calls TranscriptAPI. The bearer key never ships to the browser.

import express from "express";

const endpoint = "https://transcriptapi.com/api/v2/youtube/transcript";
export const app = express();

app.get("/api/transcript", async (req, res) => {
  const videoUrl = typeof req.query.video_url === "string" ? req.query.video_url : "";
  if (!videoUrl) {
    res.status(400).json({ error: "video_url is required" });
    return;
  }

  const apiKey = process.env.TRANSCRIPTAPI_KEY;
  if (!apiKey) {
    res.status(500).json({ error: "Server is missing TRANSCRIPTAPI_KEY" });
    return;
  }

  const url = new URL(endpoint);
  url.searchParams.set("video_url", videoUrl);
  url.searchParams.set("format", "json");
  url.searchParams.set("include_timestamp", "true");

  const upstream = await fetch(url, {
    headers: {
      Authorization: `Bearer ${apiKey}`
    }
  });

  const body = await upstream.text();
  res.status(upstream.status);
  res.type(upstream.headers.get("content-type") ?? "application/json");
  res.send(body);
});

Add input validation, rate limiting, and user authorization for your own app before exposing this route publicly. The core rule stays the same: TranscriptAPI credentials live on the server.

Frontend fetch to your own route

This is the only browser-side pattern you should ship. It calls your local route, not TranscriptAPI.

export async function loadTranscript(videoUrl) {
  const url = new URL("/api/transcript", window.location.origin);
  url.searchParams.set("video_url", videoUrl);

  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`Local transcript route failed with ${response.status}`);
  }

  const data = await response.json();
  return data.transcript.map((segment) => segment.text).join(" ");
}

Open-source libraries vs a managed transcript API

When open source is the right choice

Open-source transcript libraries are a good fit for local experiments, hobby scripts, prototypes, classroom projects, and low-volume jobs where free no-key usage matters more than managed infrastructure.

The Python youtube-transcript-api project can retrieve transcripts and subtitles for a given YouTube video, works with automatically generated subtitles, supports translation workflows, and does not require a headless browser. JavaScript and TypeScript npm alternatives also exist, including packages such as youtube-transcript and youtube-transcript-api. Review each package's README, maintenance history, license, and failure modes before you choose one.

If you are building a one-off script or processing a small personal set of videos, an OSS package may be the better first choice.

When TranscriptAPI is the better fit

Use TranscriptAPI when the transcript layer is part of a production server, customer-facing app, AI workflow, search index, channel monitor, or playlist pipeline. The managed API gives you one REST interface, bearer-token auth, structured transcript segments, search/channel/playlist endpoints, MCP workflows, and successful-request crediting.

That does not make OSS bad. It changes who owns the operational burden. The most common reason teams move off a local package is reliability once it leaves the laptop: YouTube increasingly blocks requests from datacenter and cloud IPs, including AWS, GCP, and similar hosts, and returns 429s. A scraper that works in local development can start failing when you deploy it to production scale. With a local package, your team owns proxies, IP rotation, retries, and upstream changes. With TranscriptAPI, you call a hosted API that runs that infrastructure for you, so the same transcript request is designed to keep working from production.

For broader implementation patterns beyond JavaScript, see how to extract YouTube transcripts programmatically. For channel and playlist workflows, start with the channel videos and search API guide. For AI assistant workflows, see the TranscriptAPI MCP docs.

Next steps

Start with the TranscriptAPI API docs. Confirm the endpoint, parameters, response formats, credit rules, rate-limit headers, and error behavior there before shipping.

If you want to test with real YouTube URLs, the current homepage offers 100 free credits with no card required. Keep the key server-side, run the fetch or axios example from your backend environment, then move the call behind an API route before connecting a browser UI.

Frequently Asked Questions

Can I call TranscriptAPI directly from browser JavaScript?
No. Public browser code must not contain your bearer key. Put the TranscriptAPI call in a server-side route, worker, or backend job, then have the browser call your own local route.
What endpoint do I use to fetch a YouTube transcript in JavaScript?
Use GET https://transcriptapi.com/api/v2/youtube/transcript with video_url as the required query parameter and Authorization: Bearer as the auth header.
Should I use fetch or axios for a YouTube Transcript API call?
Use native fetch for simple Node 20+ scripts, workers, and route handlers. Use axios if your codebase already relies on axios interceptors, defaults, timeout handling, or shared error utilities.
What is the TypeScript response shape for the default JSON transcript response?
The default JSON response has video_id as a string, language as a string, and transcript as an array of segments. Each segment has text, start, and duration fields.
How should JavaScript code handle 429 rate limits?
Read Retry-After when it is present, otherwise use capped exponential backoff with a small maximum retry count. Do not retry indefinitely, and keep logs free of API keys.
Do failed TranscriptAPI requests consume credits?
No. TranscriptAPI charges credits for successful requests. Failed 4xx, 5xx, and 429 responses cost 0 credits.
When should I use an open-source YouTube transcript library instead of a managed API?
Use open source for local experiments, hobby scripts, prototypes, and low-volume jobs where free no-key usage is the priority. Use TranscriptAPI when you need a production server API, managed infrastructure, search, channel, playlist, or MCP workflows.
Share