# YouTube Transcript Scraper — Subtitles, SRT/VTT & RAG Chunks (`spool/youtube-transcript-api`) Actor

Scrape YouTube transcripts and subtitles from any video, playlist or channel. Export text, SRT, VTT or timestamped chunks for RAG and LLM datasets.

- **URL**: https://apify.com/spool/youtube-transcript-api.md
- **Developed by:** [Spool](https://apify.com/spool) (community)
- **Categories:** AI, Videos, Developer tools
- **Stats:** 1 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $3.00 / 1,000 results

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/actors/running/actors-in-store.md#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 Transcript Scraper — Subtitles, SRT/VTT & RAG Chunks

**A hosted YouTube transcript API. Turn any video, playlist, or entire channel
into clean, chunked, timestamped text your AI can actually use.**

Free transcript libraries work fine until YouTube starts blocking you — then the
caption URL quietly returns an empty body with no error, and you are debugging
silence. This pulls captions through YouTube's own player endpoint on rotating
proxies, so it keeps working at volume.

It also returns embedding-ready chunks with the timestamps still attached — so
when your chatbot cites a source, it can link to the exact second in the video.

***

### What you get

- **Feed it a whole channel.** Paste a channel or playlist URL and it expands
  into every video automatically. No collecting video IDs by hand — which is what
  you actually want when building a knowledge base.
- **RAG-ready chunks out of the box.** Configurable size and overlap, so
  sentences are never cut in half at a chunk boundary. Drop straight into
  Pinecone, Qdrant, Chroma, Weaviate or any vector store.
- **Timestamps that survive chunking.** Every chunk carries `startTime` and
  `endTime`. Your retrieved passage links back to `youtube.com/watch?v=...&t=142s`.
- **Metadata for citations.** Title, channel, duration, view count, description
  and keywords come with every transcript, so your sources look credible.
- **SRT and VTT subtitle files.** Ready to drop into a video editor, a player,
  or an HTML5 `<track>` tag — no conversion step.
- **Translate into any supported language.** Uses YouTube's own caption
  translation, so timings stay exact — only the words change.

Works on regular videos, Shorts, and past live streams, in **any of the 100+
languages YouTube supports** — including auto-generated captions.

***

### Example output

```json
{
  "videoId": "aircAruvnKk",
  "url": "https://www.youtube.com/watch?v=aircAruvnKk",
  "title": "But what is a neural network? | Deep learning chapter 1",
  "channel": "3Blue1Brown",
  "durationSeconds": 1120,
  "viewCount": 24038646,
  "language": "en",
  "isAutoGenerated": false,
  "wordCount": 3357,
  "characterCount": 18430,
  "chunkCount": 29,
  "transcript": "This is a 3. It's sloppily written and rendered at an extremely low resolution of 28x28 pixels, but your brain has no trouble recognizing it as a 3...",
  "chunks": [
    {
      "index": 0,
      "text": "This is a 3. It's sloppily written and rendered at an extremely low resolution...",
      "charCount": 726,
      "startTime": 4.22,
      "endTime": 48.34
    },
    {
      "index": 1,
      "text": "But something in that crazy-smart visual cortex of yours resolves these as...",
      "charCount": 796,
      "startTime": 37.52,
      "endTime": 84.46
    }
  ],
  "availableLanguages": [
    { "languageCode": "en", "name": "English", "isAutoGenerated": false },
    { "languageCode": "es", "name": "Spanish", "isAutoGenerated": false }
  ]
}
```

Note how chunk 1 starts at **37.52s** while chunk 0 runs to **48.34s** — that is
the overlap doing its job. No sentence gets orphaned.

***

### Common uses

| You want to | How |
|---|---|
| Build a chatbot over a YouTube channel | Paste the channel URL, chunk at 1000 / overlap 150, embed `chunks[].text` |
| Ingest a course or lecture series | Paste the playlist URL |
| Summarise long talks | `outputFormat: "text"`, feed `transcript` to an LLM |
| Make searchable video archives | Keep `chunks` + `startTime` for deep links |
| Generate subtitles or captions | `subtitleFormat: "srt"` or `"vtt"` — files come back ready to use |
| Translate a video's captions | `translateTo: "fr"` — timings are preserved exactly |
| Localise a channel | `translateTo` plus a channel URL |
| Analyse competitors' videos | Batch URLs, read `transcript` and `keywords` |

***

### Input

The only required field is **`videoUrls`**. Everything else has sensible defaults.

```json
{
  "videoUrls": [
    "https://www.youtube.com/@3blue1brown",
    "https://www.youtube.com/playlist?list=PLZHQObOWTQDNU6R1_67000Dx_ZCJB-3pi",
    "https://www.youtube.com/watch?v=aircAruvnKk"
  ],
  "languages": ["en"],
  "maxVideosPerSource": 50,
  "chunkTranscript": true,
  "chunkSize": 1000,
  "chunkOverlap": 150
}
```

Mix and match freely — the input accepts:

| Type | Examples |
|---|---|
| **Channel** | `youtube.com/@handle` · `/channel/UC...` · `/c/name` |
| **Playlist** | `youtube.com/playlist?list=PL...` · any URL with `?list=` |
| **Video** | watch URLs, `youtu.be`, Shorts, embed, live, or a bare 11-char ID |

Channels and playlists expand into their videos automatically. Duplicates are
removed across all sources, so overlapping inputs never cost you twice.

#### Options

| Option | Default | What it does |
|---|---|---|
| `videoUrls` | — | **Required.** Videos to transcribe |
| `languages` | `["en"]` | Language priority order. Human-written captions beat auto-generated ones in the same language. If the video has **none** of your languages, you get whatever it does have rather than nothing — the row is flagged `languageIsFallback: true` and the log says so, so a French request answered in English is never silent |
| `includeAutoGenerated` | `true` | Allow YouTube's speech recognition when no human captions exist |
| `translateTo` | `""` | Language code to translate into, e.g. `fr`, `ar`, `es`. Empty keeps the original. YouTube's translation endpoint refuses a good share of requests, so this retries before giving up. If it still won't translate, you get the **original transcript** rather than an empty result, with `translationFailed: true` on the row and a warning in the log — never a silent swap |
| `subtitleFormat` | `none` | `srt`, `vtt`, `both`, or `none` |
| `maxVideosPerSource` | `50` | Cap per channel/playlist. Individual video URLs are never capped |
| `chunkTranscript` | `true` | Produce RAG chunks |
| `chunkSize` | `1000` | Target characters per chunk (~250 tokens) |
| `chunkOverlap` | `150` | Characters repeated across boundaries |
| `outputFormat` | `both` | `both`, `text`, or `chunks` |
| `includeSegments` | `false` | Add raw per-cue timings. Makes output much larger |
| `maxConcurrency` | `3` | Videos fetched in parallel. Raise it if your runs stay clean |
| `maxRetries` | `4` | Retries for transient failures. Blocked retries switch to residential proxy automatically |
| `proxyConfiguration` | Apify Proxy | YouTube blocks bare datacenter IPs |

#### Subtitle output

Set `subtitleFormat` and each record gains an `srt` and/or `vtt` string:

```
1
00:00:01,200 --> 00:00:03,360
Bueno, aqui estamos, frente a los elefantes.

2
00:00:05,318 --> 00:00:07,974
Lo genial de estos tipos es que tienen trompas muy...
```

Cue timings are clamped so a cue never overlaps the next one — auto-generated
tracks frequently do, and most players handle that badly.

#### Choosing chunk size

| Chunk size | Good for |
|---|---|
| 500 | Precise retrieval, short answers |
| **1000** | **General RAG — recommended starting point** |
| 2000+ | Long-context models, summarisation |

Keep overlap at roughly 10–20% of chunk size.

***

### When a video fails

Failures are returned as rows rather than silently dropped, each with a typed
`error` so you can filter and act on them:

| `error` | Meaning |
|---|---|
| `no_captions` | The video genuinely has no captions in any language |
| `language_not_available` | The video has no usable caption track at all. A video that simply lacks *your* language is not an error — see `languages` above |
| `video_unavailable` | Deleted, region-blocked, or a bad ID |
| `video_private` | Private video |
| `age_restricted` | Sign-in required |
| `live_stream` | Currently live — try again once it ends |
| `bot_detected` | YouTube flagged the exit IP. Retried automatically on residential |
| `blocked_empty_response` | YouTube throttled the request. Retried automatically on residential |
| `rate_limited` | Too many requests from one IP. Backed off and retried on residential; lower `maxConcurrency` if persistent |

A run summary with the full error breakdown is written to the key-value store as
`RUN_SUMMARY`, so you can monitor success rate over time.

**Success rates below 100% are usually normal** — plenty of YouTube videos simply
have no captions. Check the `failures` view in the dataset to see which and why.

***

### Notes

- Only **public** videos with captions are supported. This tool does not
  transcribe audio — it retrieves captions that already exist on YouTube.
- Private, members-only, and deleted videos cannot be accessed.
- Runs entirely over HTTP with no headless browser, which keeps it fast and cheap.

***

### FAQ

**How do I get a transcript from a YouTube video?**
Paste the video URL into `videoUrls` and press Start. You get the full text,
timestamped segments, and optional SRT/VTT files. No API key, no login, nothing
to install.

**Why does the `youtube-transcript-api` Python library stop working?**
Because YouTube soft-blocks it. The caption URL keeps returning HTTP 200 with an
empty body, so nothing raises an error — you simply get nothing back. This Actor
reads captions through YouTube's own player endpoint on rotating proxies, which
is why it keeps working at volume.

**Can I get transcripts for an entire channel or playlist?**
Yes. Paste a channel or playlist URL and it expands into the videos itself.
`maxVideosPerSource` caps how many.

**Which languages are supported?**
Any caption language the video actually has, plus YouTube's own translation via
`translateTo`. If the video has none of your languages you get what it does
have, flagged `languageIsFallback: true` — never a silent swap.

**What if the video has no captions at all?**
You get a row carrying a typed `error` rather than a missing entry, so a large
batch never leaves you guessing which videos worked.

**Can it produce SRT or VTT subtitle files?**
Yes — set `subtitleFormat` to `srt`, `vtt` or `both`. The files are saved to the
key-value store, ready for a video editor or an HTML5 `<track>` tag.

**How much does it cost to run?**
Pay per event, so you are charged for what you actually extract rather than for
time spent. A single video costs a fraction of a cent.

**Is this legal?**
It reads publicly available captions from public videos. No login, no private
data, no personal information. You are responsible for how you use the output.

***

### Support

Found a bug or need a feature? Open an issue on the Actor's **Issues** tab and
you will get a reply — usually the same day.

# Actor input Schema

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

Paste any mix of: video URLs or IDs, playlist URLs, channel URLs (`/@handle`, `/channel/UC...`, `/c/name`). Channels and playlists are expanded automatically into their videos. Accepts watch links, youtu.be, Shorts, embed and live URLs. Duplicates are removed.

## `languages` (type: `array`):

Language codes in priority order, e.g. `en`, `es`, `fr`. The first available match wins; a manually written track is always preferred over an auto-generated one in the same language.

## `includeAutoGenerated` (type: `boolean`):

Use YouTube's automatic speech recognition when no human-written captions exist. Turn this off if you only want human-verified text.

## `removeCaptionCredits` (type: `boolean`):

Strip translator and reviewer credits from the start and end of the transcript (e.g. "Translator: … Reviewer: …", "Subtitles by the Amara.org community"). These are not spoken content and pollute the first RAG chunk. Only the first and last few cues are checked.

## `translateTo` (type: `string`):

Language code to translate captions into, e.g. `fr`, `ar`, `es`, `de`. Uses YouTube's own caption translation, so timestamps are preserved exactly. Leave empty for the original language. Ignored for tracks YouTube marks as untranslatable.

## `maxVideosPerSource` (type: `integer`):

Caps how many videos are taken from EACH channel or playlist. Protects you from accidentally billing a 5,000-video channel. Individual video URLs are never capped.

## `chunkTranscript` (type: `boolean`):

Produce overlapping chunks sized for embedding models. Each chunk keeps its start and end timestamp so retrieved passages stay citable back to the exact moment in the video.

## `chunkSize` (type: `integer`):

Target characters per chunk. Roughly 1000 characters is about 250 tokens, which suits most embedding models.

## `chunkOverlap` (type: `integer`):

Characters repeated between consecutive chunks so a sentence split across a boundary is not lost. Typically 10-20% of chunk size.

## `outputFormat` (type: `string`):

What text to include in each result.

## `includeSegments` (type: `boolean`):

Add the caption cue list with per-line start times and durations. Useful for building subtitle files or precise seeking; makes the dataset considerably larger.

## `subtitleFormat` (type: `string`):

Also produce ready-to-use subtitle files. SRT suits most video editors and players; VTT is the web standard for HTML5 video.

## `maxConcurrency` (type: `integer`):

How many videos to fetch in parallel. Higher values are faster but more likely to hit YouTube rate limits. 3 is a safe default; raise it if your runs stay clean.

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

Retry attempts for transient failures such as rate limiting or IP blocks. Blocked retries automatically switch to residential proxy. Permanent conditions (no captions, private video) are never retried.

## `proxyConfiguration` (type: `object`):

YouTube blocks plain datacenter IPs, so Apify Proxy is enabled by default. Residential proxy improves reliability on large runs.

## Actor input object example

```json
{
  "videoUrls": [
    "https://www.youtube.com/@3blue1brown",
    "https://www.youtube.com/playlist?list=PLZHQObOWTQDNU6R1_67000Dx_ZCJB-3pi",
    "https://www.youtube.com/watch?v=aircAruvnKk"
  ],
  "languages": [
    "en"
  ],
  "includeAutoGenerated": true,
  "removeCaptionCredits": true,
  "translateTo": "fr",
  "maxVideosPerSource": 50,
  "chunkTranscript": true,
  "chunkSize": 1000,
  "chunkOverlap": 150,
  "outputFormat": "both",
  "includeSegments": false,
  "subtitleFormat": "none",
  "maxConcurrency": 3,
  "maxRetries": 4,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

All transcript records, one per video. Each contains the full text, overlapping RAG chunks with start and end timestamps, and citation metadata (title, channel, duration, view count).

## `chunksOnly` (type: `string`):

The same records with only the chunk array and identifying fields, ready to feed straight into an embedding pipeline or vector database.

## `runSummary` (type: `string`):

Counts of succeeded and failed videos, overall success rate, total characters and chunks produced, and a breakdown of failures by error type.

# 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/playlist?list=PLZHQObOWTQDNU6R1_67000Dx_ZCJB-3pi"
    ],
    "languages": [
        "en"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("spool/youtube-transcript-api").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/playlist?list=PLZHQObOWTQDNU6R1_67000Dx_ZCJB-3pi"],
    "languages": ["en"],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("spool/youtube-transcript-api").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/playlist?list=PLZHQObOWTQDNU6R1_67000Dx_ZCJB-3pi"
  ],
  "languages": [
    "en"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call spool/youtube-transcript-api --silent --output-dataset

```

## MCP server setup

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

```

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/moEL0kk4g7jFZ2Ifb/builds/067f9YJUiWdUjDauZ/openapi.json
