# YouTube Transcript & Subtitles Extractor - Text, SRT, VTT (`readable_slash/youtube-transcript-extractor`) Actor

Extract YouTube transcripts and subtitles in bulk. Returns plain text, timestamped segments, SRT, VTT, or LLM-ready chunks. Handles auto-generated and human captions in any language. Pay only for videos that return a transcript.

- **URL**: https://apify.com/readable\_slash/youtube-transcript-extractor.md
- **Developed by:** [HJL Analytics and AI development](https://apify.com/readable_slash) (community)
- **Categories:** Videos, AI, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 transcript fetcheds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## YouTube Subtitles & Captions Scraper — Text, SRT, VTT

Download the subtitles of any YouTube video, in bulk, as plain text, timestamped segments, SRT, WebVTT, or chunks ready to feed a language model.

Paste video URLs, get subtitles back. No API key, no browser, no OAuth. **You are only charged for videos that actually return a transcript.**

### What it does

- **Download YouTube subtitles in bulk** — hundreds of videos in one run
- **Save YouTube captions as an SRT or VTT subtitle file**
- **Get the subtitle track in any language**, auto-generated or human-written
- **Extract a YouTube transcript as plain text** for summarising, searching, or translating
- **Convert a YouTube video to text** without downloading the video itself
- **Prepare subtitles for RAG and LLM pipelines**, chunked with timestamps preserved
- Accepts watch links, `youtu.be` short links, Shorts, embeds, live URLs, or bare video IDs

Works with every subtitle track YouTube exposes: manually written captions, auto-generated ones, and translated subtitle tracks in any of the languages a video offers.

### Output

One row per video. A 19-minute talk looks like this:

```json
{
  "videoId": "aircAruvnKk",
  "status": "ok",
  "title": "But what is a neural network? | Deep learning chapter 1",
  "channel": "3Blue1Brown",
  "durationSeconds": 1120,
  "viewCount": 23919789,
  "language": "en",
  "isGenerated": false,
  "languageMatch": "exact-human",
  "cueCount": 286,
  "wordCount": 3357,
  "text": "This is a 3. It's sloppily written and rendered at an extremely low resolution...",
  "chunks": [
    { "index": 0, "text": "This is a 3...", "startSeconds": 4.22, "endSeconds": 71.5, "estimatedTokens": 291 }
  ]
}
```

### Input

| Field | Description |
|---|---|
| `videoUrls` | Video URLs or 11-character IDs. Duplicates are removed, so the same video pasted twice is charged once. |
| `preferredLanguages` | Language codes in order, e.g. `["nl", "en"]`. Defaults to `["en"]`. |
| `outputFormats` | Any of `text`, `segments`, `chunks`, `srt`, `vtt`. Only what you ask for is returned. |
| `chunkTargetChars` | Target chunk size when using `chunks`. Default 1200. |
| `chunkOverlapChars` | Context repeated at each chunk start. Default 100. |
| `includeAvailableLanguages` | List every caption language the video offers. |
| `useResidentialProxy` | On by default and effectively required — see the note below. |
| `proxyCountry` | Optional two-letter country code, e.g. `US` or `NL`. |
| `maxVideos` | Safety cap. 0 means no limit. |
| `concurrency` | Videos fetched at once. Default 5. |

### How language selection works

Getting the wrong track is the usual reason a transcript is useless, so the order is explicit:

1. Your requested language, human-written
2. Your requested language, auto-generated
3. A regional variant of it — `en-GB` when you asked for `en`
4. Any human-written track
5. Anything available

Each result reports which rule applied in `languageMatch`, so you can tell a precise hit from a fallback rather than guessing.

Human-written beats auto-generated at the same language on purpose: machine captions carry no punctuation and mangle proper nouns, which is exactly what makes a transcript useless further down a pipeline.

### LLM-ready chunks

Selecting `chunks` merges caption fragments into passages of roughly your target size, breaking at sentence ends where possible and keeping the timestamp each passage started at.

That timestamp is the point. It means an answer generated from a chunk can be traced back to a moment in the video, instead of being an unattributable claim. Each chunk also carries a token estimate so you can budget a model call before making it.

### When a video fails

Failures are rows, not crashes. One private video in a batch of 500 leaves you with 499 transcripts and one explained failure — and you are not billed for the failure.

| `errorCode` | What it means | Retried |
|---|---|---|
| `NO_CAPTIONS` | The video has no caption tracks, or is unavailable or deleted | no |
| `LOGIN_REQUIRED` | The video is private or age-restricted and needs an account | no |
| `BOT_CHECK` | YouTube challenged the address the request came from | yes |
| `NETWORK_ERROR` | The request never reached YouTube | yes |
| `TIMEOUT` | YouTube did not answer in time | yes |
| `CAPTIONS_GATED` | Captions exist but sit behind YouTube's proof-of-origin gate | no |
| `EMPTY_RESPONSE` | YouTube accepted the request and returned nothing, which means the same gate | no |
| `EMPTY_TRANSCRIPT` | The track downloaded but contained no text | no |
| `UNREADABLE_INPUT` | The input was not a YouTube video URL or ID | no |

Retryable failures are attempted again from a different proxy address, up to `maxRetries` times. Permanent ones are not, so a private video never wastes your time or money on retries.

`BOT_CHECK` and `LOGIN_REQUIRED` are worth distinguishing because YouTube reports both with the same status code and only the message differs: one means the video needs an account, the other means the exit address was under suspicion. Conflating them fails videos that are perfectly fine.

### Honest notes

**Playlists and channels are not supported.** Supply individual videos. A channel URL returns a clear message rather than a vague parse error.

**This relies on YouTube's internal player API**, which is undocumented and changes without notice. As of 2026 YouTube gates caption downloads behind a proof-of-origin token that ordinary requests cannot mint; this actor uses a client that is not subject to that gate, and falls back through several others when one stops working. If YouTube closes all of them, transcripts will fail and the actor will need updating — the run log says so explicitly rather than returning an empty result and leaving you to guess.

**A residential proxy is required and is on by default.** YouTube refuses datacenter address ranges outright, answering every request with "Sign in to confirm you're not a bot". This was measured, not assumed: from a datacenter connection zero of three caption-bearing videos returned a transcript, and the identical code through a residential exit returned all three. Each video uses its own proxy session so a large batch leaves from many addresses rather than concentrating behind one.

**Auto-generated captions are approximate.** They have no punctuation, guess at proper nouns, and struggle with accents and music. Every result flags `isGenerated` so you can decide whether that matters for your use.

### Local development

```bash
npm install
npm test                                              # 40 unit tests, no network
node cli.js https://youtu.be/aircAruvnKk --format text,chunks
node cli.js VIDEO_ID_1 VIDEO_ID_2 --lang nl,en
```

The CLI runs the same pipeline as the actor, so breakage can be diagnosed locally instead of by spending platform credit.

# Actor input Schema

## `videoUrls` (type: `array`):

Video URLs or 11-character video IDs. Watch links, youtu.be short links, Shorts, embeds and live URLs all work. Duplicates are removed, so the same video pasted twice is only charged once.

## `preferredLanguages` (type: `array`):

Language codes in order of preference, for example nl, en, de. A human-written track always wins over an auto-generated one in the same language. If none of the preferred languages exist, the closest regional variant is used, then any human track, then anything available.

## `outputFormats` (type: `array`):

Which shapes to return. Only what you request is included, so a long video does not arrive in five formats at once.

## `chunkTargetChars` (type: `integer`):

Target size for each chunk when the chunks format is selected. Chunks break at sentence ends where possible and keep the timestamp they started at, so a model's answer can be traced back to a moment in the video.

## `chunkOverlapChars` (type: `integer`):

How much text to repeat at the start of each chunk. A small overlap helps retrieval when a passage begins mid-topic. Set to 0 to disable.

## `includeAvailableLanguages` (type: `boolean`):

Adds every caption language the video offers to each result. Useful when deciding which languages to request on a later run.

## `useResidentialProxy` (type: `boolean`):

Required. YouTube refuses requests from datacenter address ranges and answers every one with 'Sign in to confirm you're not a bot'. Turning this off will make every video fail unless you are running the code on a home connection.

## `proxyCountry` (type: `string`):

Optional two-letter country code, for example US or NL. Leave empty to let the pool choose. Setting one can matter for region-restricted videos.

## `maxRetries` (type: `integer`):

How many times to retry a video that failed for a network reason, each time from a different proxy address. Videos that genuinely have no captions are never retried, so raising this does not waste time or money on them.

## `maxVideos` (type: `integer`):

Safety cap on how many videos to process. 0 means no limit.

## `concurrency` (type: `integer`):

How many videos to fetch at once. Raising this speeds up large runs; lowering it is gentler on YouTube.

## Actor input object example

```json
{
  "videoUrls": [
    "https://www.youtube.com/watch?v=aircAruvnKk",
    "https://www.youtube.com/watch?v=jNQXAC9IVRw"
  ],
  "preferredLanguages": [
    "en"
  ],
  "outputFormats": [
    "text"
  ],
  "chunkTargetChars": 1200,
  "chunkOverlapChars": 100,
  "includeAvailableLanguages": false,
  "useResidentialProxy": true,
  "proxyCountry": "",
  "maxRetries": 3,
  "maxVideos": 0,
  "concurrency": 5
}
```

# Actor output Schema

## `transcripts` (type: `string`):

Every processed video with its transcript, language and metadata.

## `transcriptsCsv` (type: `string`):

The same results as a spreadsheet.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "videoUrls": [
        "https://www.youtube.com/watch?v=aircAruvnKk",
        "https://www.youtube.com/watch?v=jNQXAC9IVRw"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("readable_slash/youtube-transcript-extractor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "videoUrls": [
        "https://www.youtube.com/watch?v=aircAruvnKk",
        "https://www.youtube.com/watch?v=jNQXAC9IVRw",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("readable_slash/youtube-transcript-extractor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "videoUrls": [
    "https://www.youtube.com/watch?v=aircAruvnKk",
    "https://www.youtube.com/watch?v=jNQXAC9IVRw"
  ]
}' |
apify call readable_slash/youtube-transcript-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,readable_slash/youtube-transcript-extractor"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/QUfN5N3TbkFHSFHLI/builds/t4yzopnZqlSIeP0Nc/openapi.json
