# YouTube Transcript Scraper - Bulk Subtitles, SRT & RAG Chunks (`datamill/youtube-transcript-scraper`) Actor

Extract YouTube transcripts in bulk from videos, playlists, channels, @handles or searches. Timestamped segments, plain text, SRT, VTT and RAG-ready chunks, in any caption language, with optional translation.

- **URL**: https://apify.com/datamill/youtube-transcript-scraper.md
- **Developed by:** [Data Mill](https://apify.com/datamill) (community)
- **Categories:** AI, Videos, Developer tools
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

### What does YouTube Transcript Scraper do?

This Actor extracts **YouTube transcripts and subtitles in bulk** and hands them back in whatever shape your pipeline needs: timestamped segments, plain text, **SRT**, **WebVTT**, or **RAG-ready chunks** with per-chunk timestamped links.

Give it anything YouTube: a video URL or ID, a `youtu.be` or `/shorts/` link, a **playlist**, a **channel URL or @handle**, or a search like `ytsearch:apache spark tutorial`. Playlists, channels and searches expand into their videos automatically. Run it in the Console, call it over the **API**, **schedule** it, or let an AI agent use it through **MCP**.

#### Why this one and not the others

Most YouTube transcript Actors read the watch page and then call `/api/timedtext`. As of 2026 **that route returns an empty body** — YouTube gates it behind a session token, which is why so many transcript tools now fail intermittently. This Actor goes through YouTube's **InnerTube API** (what the official mobile apps use), falls back across seven clients, and runs on a residential proxy by default because YouTube refuses datacenter IPs outright. That combination is why transcripts keep arriving.

| | This Actor | Typical alternative |
|---|---|---|
| Input | Videos, playlists, channels, @handles, search | One video URL per run |
| Output | Segments, text, SRT, VTT, RAG chunks | Segments only |
| Languages | Pick priority order, see all available tracks | Whatever comes first |
| Translation | YouTube auto-translation to any language | None |
| Metadata | Title, channel, duration, views, thumbnail, tags | None |
| Failures | Per-video status, run keeps going | Run dies |

### Why use YouTube Transcript Scraper?

- **Feed RAG pipelines and LLMs.** The `chunks` format returns overlapping passages sized for embedding models, each carrying `startSeconds`, `endSeconds` and a deep link back to that moment in the video, so your answers can cite the exact timestamp.
- **Repurpose content at scale.** Turn an entire channel's back catalogue into blog drafts, summaries or newsletters in one run.
- **Ship subtitles.** Download ready-to-use `.srt` or `.vtt` files, including YouTube-translated versions.
- **Research and analysis.** Search a topic, pull every transcript, and analyse what is actually being said across creators.
- **Accessibility and SEO.** Publish text versions of video content that search engines can index.

### How to use YouTube Transcript Scraper

1. Click **Try for free**.
2. Paste your sources into **YouTube videos, playlists, channels or searches**. Mix types freely — one video, a whole channel, and a search can all go in the same run.
3. Set **Max videos** so a large channel cannot surprise you.
4. Choose your **Output formats**. Pick `chunks` if this is going into a vector database.
5. Click **Start**, then export from the **Output** tab as JSON, CSV, Excel or HTML.

### Input

| Field | Type | Description |
|---|---|---|
| `videoUrls` | array | Videos, IDs, playlists, channels, `@handles`, or `ytsearch:query` |
| `maxVideos` | integer | Cap on videos processed per run |
| `preferredLanguages` | array | Language codes in priority order, e.g. `["ja","en"]` |
| `includeAutoGenerated` | boolean | Accept YouTube's automatic captions (default on) |
| `translateTo` | string | Have YouTube translate captions, e.g. `ja` |
| `outputFormats` | array | `segments`, `text`, `srt`, `vtt`, `chunks` |
| `chunkCharacters` | integer | Target chunk size for RAG (default 1200) |
| `chunkOverlapCharacters` | integer | Overlap between chunks (default 150) |
| `includeMetadata` | boolean | Title, channel, duration, views, thumbnail |
| `includeDescription` | boolean | Full description and tags (off by default) |
| `skipVideosWithoutCaptions` | boolean | Omit caption-less videos entirely |

```json
{
  "videoUrls": ["https://www.youtube.com/@MrBeast", "ytsearch:apache spark tutorial"],
  "maxVideos": 50,
  "preferredLanguages": ["en"],
  "outputFormats": ["text", "chunks"],
  "chunkCharacters": 1200
}
```

### Output

One item per video. You can download the dataset in various formats such as JSON, HTML, CSV, or Excel.

```json
{
  "videoId": "jNQXAC9IVRw",
  "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
  "status": "ok",
  "title": "Me at the zoo",
  "channel": "jawed",
  "channelUrl": "https://www.youtube.com/channel/UC4QobU6STFB0P71PMvOGN5A",
  "durationSeconds": 19,
  "viewCount": 403777047,
  "language": "en",
  "languageName": "English",
  "isAutoGenerated": false,
  "availableLanguages": ["en", "de"],
  "wordCount": 39,
  "segmentCount": 6,
  "text": "All right, so here we are, in front of the elephants ...",
  "transcript": [
    { "startSeconds": 1.2, "durationSeconds": 4.118, "text": "All right, so here we are, in front of the elephants" }
  ],
  "chunks": [
    {
      "index": 0,
      "startSeconds": 1.2,
      "endSeconds": 18.88,
      "text": "All right, so here we are, in front of the elephants ...",
      "charCount": 212,
      "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw&t=1s"
    }
  ]
}
```

### Data you can extract

| Field | Description |
|---|---|
| `videoId`, `url` | Video identifier and canonical link |
| `status` | `ok`, `no_captions`, `unavailable`, `network_error` or `invalid_input` |
| `title`, `channel`, `channelId`, `channelUrl` | Video and creator identity |
| `durationSeconds`, `viewCount`, `thumbnailUrl`, `isLive` | Video metadata |
| `description`, `keywords` | Full description and tags (opt-in) |
| `language`, `languageName`, `isAutoGenerated` | Which caption track was used |
| `availableLanguages` | Every caption language the video offers |
| `translatedTo`, `translationError` | Translation target, and why it fell back if it did |
| `transcript[]` | `startSeconds`, `durationSeconds`, `text` per caption line |
| `text` | Whole transcript as one string |
| `srt`, `vtt` | Ready-to-save subtitle files |
| `chunks[]` | RAG passages with timestamps and deep links |
| `wordCount`, `characterCount`, `segmentCount`, `chunkCount` | Size counters |

### How much does it cost to scrape YouTube transcripts?

Pay per event, charged only when a transcript is actually returned:

| Event | Price |
|---|---|
| Transcript extracted | $0.004 |
| Actor start | $0.00005 |

Videos with no captions, private videos and invalid inputs are reported in the dataset but **not charged**. 1,000 transcripts cost about **$4**, whichever formats you request — cheaper than every comparable Actor on the Store.

#### One thing to leave switched on

YouTube refuses datacenter IPs with *"Sign in to confirm you're not a bot"*, so this Actor ships with **Apify residential proxy enabled by default**. Leave it on. Residential traffic is billed by Apify at roughly **$0.001 per transcript** on top of the price above, and without it every run fails. Actors that do not use a residential proxy are the reason transcript scraping has a reputation for being flaky.

### Tips

- **Building a vector database?** Use `outputFormats: ["chunks"]` alone. You skip the duplicate `text` and `segments` payloads, and every chunk already carries the timestamp link you need for citations.
- **Want human-written subtitles only?** Turn off `includeAutoGenerated`. Videos that only have automatic captions will come back as `no_captions` instead of lower-quality text.
- **Multilingual channels:** put several codes in `preferredLanguages` (`["ja","en"]`) and the first available track wins. `availableLanguages` on every item tells you what else was there.
- **Large channels:** raise `maxVideos` and keep `maxConcurrency` at 5. If you start seeing `network_error`, lower concurrency rather than raising it.
- Translation is throttled by YouTube more aggressively than plain captions. If it is rate limited you still get the original transcript, with the reason in `translationError`.

### FAQ

**Which videos work?**
Any public video that has captions, automatic or human-written. Private, deleted, members-only and age-restricted videos return `unavailable` with a reason.

**Does it need an API key or login?**
No. No YouTube Data API quota, no OAuth, no cookies.

**Can it transcribe videos that have no captions?**
No. This Actor extracts captions that already exist; it does not run speech-to-text. Check `status: "no_captions"` to find those videos.

**Is scraping YouTube transcripts legal?**
The Actor reads publicly available caption data without bypassing any login or paywall. Web scraping of public data is generally lawful in many jurisdictions, but you are responsible for complying with YouTube's terms and the copyright of the material you process.

**Something broke, or you need another field?**
Use the **Issues** tab. YouTube changes its internals regularly, and fixes usually ship within a day.

### Other Actors by Data Mill

- [Mercari Sold Price Scraper](https://apify.com/datamill/mercari-sold-price-scraper) — verified sold prices across Mercari, Yahoo Auctions and PayPay Flea Market
- [Japan Hotel Scraper](https://apify.com/datamill/jalan-hotel-scraper) — Jalan rates, ratings and plans, including ryokan the global OTAs never list
- [Japan Horse Racing Scraper](https://apify.com/datamill/netkeiba-race-scraper) — JRA race cards, live odds and results with full payouts

All of them return clean English-keyed JSON and are maintained the same way.

# Actor input Schema

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

Paste anything YouTube: video URLs or IDs, `youtu.be` and `/shorts/` links, playlist URLs, channel URLs, `@handles`, or a search with `ytsearch:your query`. Playlists, channels and searches are expanded into their videos automatically, up to **Max videos**.

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

Hard cap on how many videos this run will process. Protects you from accidentally pulling an entire channel.

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

Language codes in priority order, e.g. `en`, `ja`, `es`. The first available track wins; `en` also matches `en-GB`. If none match, the video's default track is used.

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

Most videos only have YouTube's automatic captions. Turn this off to accept human-written subtitles only.

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

Optional language code (e.g. `ja`, `es`, `de`) to have YouTube translate the captions. If translation is rate limited, you still get the original transcript plus a `translationError` note.

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

Which representations to include per video. `chunks` produces overlapping passages sized for embeddings, each with its own timestamped YouTube link.

## `chunkCharacters` (type: `integer`):

Target size of each RAG chunk. Only used when `chunks` is selected. Chunks never split a caption line, so timestamps stay exact.

## `chunkOverlapCharacters` (type: `integer`):

How much text each chunk repeats from the previous one, so meaning is not cut at a boundary.

## `includeMetadata` (type: `boolean`):

Adds title, channel, duration, view count and thumbnail to every item.

## `includeDescription` (type: `boolean`):

Adds the full video description and tags. Off by default because descriptions can be very long.

## `skipVideosWithoutCaptions` (type: `boolean`):

When on, videos with no captions are left out of the dataset entirely instead of being reported with a `no_captions` status.

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

Videos fetched in parallel. Lower this if you hit YouTube rate limits on very large runs.

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

**Keep residential proxy enabled.** YouTube blocks datacenter IPs with "Sign in to confirm you're not a bot", so runs without a residential proxy will fail. Residential traffic costs roughly $0.0005 per transcript.

## Actor input object example

```json
{
  "videoUrls": [
    "https://www.youtube.com/watch?v=jNQXAC9IVRw"
  ],
  "maxVideos": 100,
  "preferredLanguages": [
    "en"
  ],
  "includeAutoGenerated": true,
  "outputFormats": [
    "segments",
    "text"
  ],
  "chunkCharacters": 1200,
  "chunkOverlapCharacters": 150,
  "includeMetadata": true,
  "includeDescription": false,
  "skipVideosWithoutCaptions": false,
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# 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=jNQXAC9IVRw"
    ],
    "preferredLanguages": [
        "en"
    ],
    "outputFormats": [
        "segments",
        "text"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("datamill/youtube-transcript-scraper").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=jNQXAC9IVRw"],
    "preferredLanguages": ["en"],
    "outputFormats": [
        "segments",
        "text",
    ],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("datamill/youtube-transcript-scraper").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=jNQXAC9IVRw"
  ],
  "preferredLanguages": [
    "en"
  ],
  "outputFormats": [
    "segments",
    "text"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call datamill/youtube-transcript-scraper --silent --output-dataset

```

## MCP server setup

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

```

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/WRrQ8dHwnjMvuEv53/builds/uKeIJTzaib4d5QJuQ/openapi.json
