# YouTube Transcripts - Bulk, Multi-Language & RAG-ready (`victor187/youtube-transcripts-bulk`) Actor

Bulk YouTube transcripts: videos, playlists and channels in ONE run - the others take one at a time. Language preference that favours human-written subtitles, RAG-ready chunks with timestamps, SRT and VTT, full metadata. Videos without subtitles are never charged.

- **URL**: https://apify.com/victor187/youtube-transcripts-bulk.md
- **Developed by:** [victor](https://apify.com/victor187) (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 $2.40 / 1,000 transcripts

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## YouTube Transcripts — Bulk, Multi-Language & RAG-ready

Give it a list of YouTube videos, playlists and channels — **mixed, in a single run** — and get back clean transcripts with timestamps, metadata, and optional chunks ready to drop straight into a vector index.

Most transcript Actors take **one** video, or **one** channel, per run. If you have 2,000 URLs to process, that is 2,000 runs and 2,000 lots of start-up overhead. This one takes the whole list at once.

### What you get

| | |
|---|---|
| **Mixed batch input** | Videos, playlists, channels and `@handles` in one run |
| **Language preference chain** | Ask for `["es","en"]` and a *human-written* Spanish subtitle wins; a human-written English one beats an auto-generated Spanish one |
| **RAG-ready chunks** | Text split at sentence boundaries where the captions have punctuation, at caption-line boundaries where they do not — never mid-word. Each chunk carries the second it starts and ends, and says whether it closed on a sentence |
| **SRT and WebVTT** | Subtitle files, not just JSON |
| **Honest failures** | Every video that has no transcript says exactly why — and is **not charged** |
| **Metadata included** | Title, channel, duration, view count, thumbnail |

### Input

Paste URLs in any of these shapes — they all work, and you can mix them:

```
https://www.youtube.com/watch?v=dQw4w9WgXcQ
https://youtu.be/dQw4w9WgXcQ
https://www.youtube.com/shorts/xxxxxxxxxxx
https://www.youtube.com/playlist?list=PL...
https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw
https://www.youtube.com/@3blue1brown
@veritasium
dQw4w9WgXcQ
```

Use `maxVideosPerSource` to take, say, the 20 most recent videos of each channel, and `maxVideos` as a hard cap on the whole run.

### Output

One record per video:

```json
{
  "video_id": "dQw4w9WgXcQ",
  "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "success": true,
  "title": "Rick Astley - Never Gonna Give You Up",
  "channel": "Rick Astley",
  "video_duration_seconds": 213,
  "view_count": 1657482913,
  "language": "en",
  "is_auto_generated": false,
  "available_languages": ["de", "en", "es", "fr", "ja", "pt"],
  "segment_count": 61,
  "character_count": 1834,
  "transcript": "We're no strangers to love…",
  "segments": [
    { "text": "We're no strangers to love", "start": 18.64, "duration": 3.2 }
  ],
  "chunks": [
    { "chunk_index": 0, "text": "…", "start": 18.64, "end": 74.1,
      "char_count": 987, "ends_sentence": true }
  ]
}
```

When a video has no usable transcript you still get a record, with `success: false` and a plain reason:

```json
{ "video_id": "…", "success": false, "error": "this video has no subtitle track at all" }
```

So a run over 1,000 videos gives you the 940 that worked **and** a clear list of the 60 that did not — instead of silently returning fewer rows than you asked for.

### Why chunks matter

If you are building search or Q\&A over video, raw caption segments are the wrong shape: each one is two seconds of half a sentence. You end up writing a chunker that merges them sensibly and keeps the timestamps so you can link an answer back to the exact moment.

Turn on `includeChunks` and that is already done.

One honest detail: auto-generated captions often contain no punctuation at all, sometimes for the whole video. Waiting for a full stop would then produce one enormous useless block, so chunks close at a caption-line boundary instead — never mid-word — and `ends_sentence` tells you which happened. You know what you are indexing.

### About the proxy

YouTube rate-limits transcript requests by IP — in our testing it starts refusing after roughly **80 videos** from one address. For anything beyond a handful of videos you need a proxy, and residential is the option that holds up. The default input is already set to Apify Residential Proxy.

This is not a quirk of this Actor; it is the reason every transcript Actor either uses proxies or fails on larger runs.

### Pricing

**$3.00 per 1,000 transcripts** — 40% below the market leader's $5.00, and it drops further on paid Apify plans:

| Your Apify plan | Price per 1,000 |
|---|---|
| Free | $3.00 |
| Bronze | $2.80 |
| Silver | $2.60 |
| Gold and above | $2.40 |

Videos without subtitles are **not charged**. If a run finds 940 transcripts out of 1,000 URLs, you pay for 940 — the 60 failures cost you nothing and each one tells you why.

### Limits, stated plainly

- Only videos that **have** subtitles, manual or auto-generated. There is no speech-to-text here; nothing can invent a transcript for a video that has none.
- Private, deleted, age-restricted and members-only videos cannot be read.
- Channels and playlists return their most recent videos; very long back-catalogues may not be returned in full.

# Actor input Schema

## `startUrls` (type: `array`):

Videos, playlists or channels — mix them freely in one run. Accepts watch URLs, youtu.be links, Shorts, /playlist?list=…, /channel/UC…, and @handles.

## `urlsText` (type: `string`):

One URL or video ID per line. Handy when you already have a list somewhere else.

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

ISO 639-1 codes in order of preference, e.g. \["es","en"]. A human-written subtitle in your second language is picked before an auto-generated one in your first.

## `allowAutoGenerated` (type: `boolean`):

If off, videos that only have machine captions are skipped (and not charged).

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

0 means no limit. Applies to each playlist or channel, not to single videos.

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

Hard cap for the whole run. 0 means no limit.

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

Higher is faster but more likely to be rate-limited. 8 is a good balance.

## `includePlainText` (type: `boolean`):

The whole transcript as one string, no timestamps.

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

Every caption line with its start time and duration.

## `includeChunks` (type: `boolean`):

Text split into chunks that never cut a sentence in half, each carrying the second it starts and ends. This is what you want when feeding a vector index — it saves you writing the chunker yourself.

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

Target size of each chunk in characters. Chunks end at a sentence boundary, so the real size varies a little around this.

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

Keeps an idea that straddles two chunks from being lost in both.

## `includeSrt` (type: `boolean`):

Also return the transcript as an SRT subtitle file, ready to load into a video editor or player.

## `includeVtt` (type: `boolean`):

Also return the transcript as a WebVTT file, the format HTML5 video uses.

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

Title, channel, duration, view count and thumbnail.

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

YouTube rate-limits by IP after roughly 80 videos, so a proxy is required for anything but tiny runs. Residential is the reliable choice.

## `diagnose` (type: `boolean`):

Internal: measure which extraction route currently works and stop.

## `diagnoseVideoId` (type: `string`):

Internal: video used by diagnostic mode.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
    }
  ],
  "languages": [
    "en"
  ],
  "allowAutoGenerated": true,
  "maxVideosPerSource": 0,
  "maxVideos": 0,
  "concurrency": 8,
  "includePlainText": true,
  "includeSegments": true,
  "includeChunks": false,
  "chunkSize": 1000,
  "chunkOverlap": 100,
  "includeSrt": false,
  "includeVtt": false,
  "includeMetadata": true,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "diagnose": false,
  "diagnoseVideoId": "dQw4w9WgXcQ"
}
```

# Actor output Schema

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

Every processed video, successful or not. Videos without subtitles carry an 'error' explaining why and are never charged.

## `chunks` (type: `string`):

One row per chunk with its start and end second — ready to load straight into a vector index.

## `plainText` (type: `string`):

Just the video id, title and full transcript, without timestamps.

## `overview` (type: `string`):

What came back for each video at a glance: language, whether the subtitles were auto-generated, and how much text.

# 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 = {
    "startUrls": [
        {
            "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
        }
    ],
    "languages": [
        "en"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("victor187/youtube-transcripts-bulk").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 = {
    "startUrls": [{ "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" }],
    "languages": ["en"],
}

# Run the Actor and wait for it to finish
run = client.actor("victor187/youtube-transcripts-bulk").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 '{
  "startUrls": [
    {
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
    }
  ],
  "languages": [
    "en"
  ]
}' |
apify call victor187/youtube-transcripts-bulk --silent --output-dataset

```

## MCP server setup

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

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/XnHSrAZTT86BX0mq9/builds/pxLuwCdR9aHsakZm9/openapi.json
