# YouTube Transcript Scraper — RAG-Ready (`vonsensey/youtube-transcript-scraper`) Actor

Turn YouTube into RAG-ready transcripts: chapter-grouped, chunk-ready paragraphs for LLM and search pipelines. Re-runs skip already-delivered videos to keep a corpus fresh, and failed videos are never billed. Accepts mixed video, playlist, and channel URLs. Unofficial; public data only.

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

## Pricing

from $5.00 / 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/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 Transcript Scraper — RAG-Ready

**More from this account:** [Website Contact & Email Suite](https://apify.com/vonsensey/website-contact-email-extractor) · [Career Site & ATS Jobs Suite](https://apify.com/vonsensey/career-page-job-postings-scraper-api) · [Google News Suite](https://apify.com/vonsensey/google-news-scraper-api) · [Keyword Research Suite](https://apify.com/vonsensey/google-keyword-ideas-scraper) · [Shopify Store Intelligence Suite](https://apify.com/vonsensey/shopify-store-leads-scraper) · [eBay Data Suite](https://apify.com/vonsensey/ebay-scraper-api) · [Amazon Reviews Suite](https://apify.com/vonsensey/amazon-reviews-scraper-api) · [Reddit](https://apify.com/vonsensey/reddit-scraper-posts-comments-api) · [Meta Ad Library](https://apify.com/vonsensey/facebook-ads-library-scraper-meta-ad-api) · [Vinted](https://apify.com/vonsensey/vinted-scraper-api)

**RAG-ready YouTube transcripts — chapter-grouped, chunk-ready paragraphs.**

Point it at YouTube videos, playlists, or channels and get back structured JSON that drops straight into a retrieval or LLM pipeline: caption text merged into clean paragraphs, grouped under the video's own chapters, with millisecond timing on every segment.

> **Unofficial — not affiliated with, endorsed by, or connected to YouTube or Google.** This actor works only with data that YouTube already serves publicly. No YouTube logo or trademark is used in its name or branding.

***

### Why this one

#### 1. Chapter-grouped, LLM-ready paragraphs

Raw caption tracks are a wall of 2–5 word cues with no sentence or topic boundaries — useless for chunking. This actor merges those cues into readable paragraphs (breaking on pauses and sentence endings, targeting ~500–1,500 characters) and groups them under the video's **chapters**, so each chunk already carries its topic and its timestamp.

```jsonc
// one chapter from a real run (abridged)
{
  "title": "Introduction",
  "startMs": 0,
  "endMs": 112480,
  "paragraphs": [
    {
      "startMs": 0,
      "endMs": 34640,
      "text": "In this course, I'm going to teach you everything you need to know to get started programming in Python. Now, Python is one of the most popular programming languages out there…"
    },
    {
      "startMs": 81200,
      "endMs": 112480,
      "text": "We're going to talk about all the core concepts in Python. We're going to look at everything you need to know to start programming in Python…"
    }
  ]
}
```

Videos without chapters still come back grouped — as a single group with a `null` title — so downstream code has one shape to handle. See [`.actor/sample-output.json`](.actor/sample-output.json) for two complete records from a real run.

#### 2. Freshness re-runs skip what you already have

Turn on **Only new videos** and give the run a **state label**. Every delivered transcript is remembered under that label, so a scheduled re-run over the same channel or playlist fetches — and bills — **only the new uploads**. Keep a corpus current without re-paying for videos you already have.

#### 3. Failures are free

Billing is **per delivered transcript** — one charge for each caption track this actor actually hands you. Every failure mode — captions disabled, region-locked, members-only, a dead playlist entry, a bot-check block — is pushed as a documented error item at **no charge.** You pay only for transcripts you receive; errors are transparent and cost nothing.

**Supported input scope:** a single run accepts a **mixed list** of video, playlist, channel, and `@handle` URLs — each playlist or channel is expanded into its videos automatically, then de-duplicated across sources.

***

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| **urls** *(required)* | array of strings | — | Mixed list of YouTube URLs: individual videos (`watch?v=`, `youtu.be/`, `/shorts/`, `/embed/`, `/live/`), playlists (`list=`), channels (`/channel/UC…`, `/@handle`, `/c/…`, `/user/…`), and channel tab URLs. Playlists and channels are expanded to their videos. A `watch?v=…&list=…` URL is treated as the single video. |
| **languages** | array of strings | *(empty)* | Ordered language preference (BCP-47 tags or prefixes, e.g. `en`, `pt-BR`). First available match wins. Empty → the video's default caption track. |
| **allLanguages** | boolean | `false` | Return **every** caption track instead of the best match. ⚠️ This emits **one dataset item per track, and each track is billed as one transcript.** Ignores the `languages` list. |
| **includeShorts** | boolean | `false` | When expanding a channel, also include Shorts. Off = long-form uploads only. |
| **maxVideosPerSource** | integer | `100` | Cap on videos taken from **each** playlist or channel URL (1–5000), applied per source. Direct video URLs are unaffected. |
| **includeChapters** | boolean | `true` | Extract chapter markers **and** the upload date. Turning it off saves one request per video but leaves `uploadDate` `null` (they share the same source call). Transcripts are still grouped — under one `null`-title chapter. |
| **formats** | array | *(empty)* | Extra rendered formats to add to each item: `text` (plain text), `srt`, `vtt`. Structured JSON segments and paragraphs are **always** included. |
| **onlyNewVideos** | boolean | `false` | Skip videos already delivered by previous runs sharing the same **state label**. For scheduled re-runs that should pick up only new uploads. |
| **stateLabel** | string | `default` | Names the persistent delivered-video memory used by *Only new videos*. Runs sharing a label share that memory. |
| **proxyConfiguration** | object | Apify Proxy | Proxy settings for YouTube requests. YouTube blocks most cloud IPs, so a residential proxy is recommended for caption fetching; you may also supply your own proxy URLs. |

> **Overlapping schedules:** *Only new videos* state is read at run start and written per delivery (best-effort). Two runs sharing a state label that overlap in time may both deliver the same brand-new video. Schedule runs on the same label so they don't overlap.

***

### Output

Every result is one JSON record in the run's dataset. A **delivered transcript** looks like this:

| Field | Type | Notes |
|---|---|---|
| `videoId` | string | e.g. `jNQXAC9IVRw`. |
| `url` | string | Canonical watch URL. |
| `title`, `channel`, `channelId` | string | Video and channel identity. |
| `uploadDate` | string | null | YouTube's own absolute date string (`hl=en`, e.g. `"Jul 11, 2018"`). `null` when `includeChapters` is off. |
| `durationSeconds` | number | Video length. |
| `language` | string | Language tag of the delivered track. |
| `matchedLanguageTag` | string | The track's BCP-47 language tag. Under `allLanguages`, duplicate same-language tracks get a `#2`/`#3` suffix to stay distinct; use `language` for the clean code. |
| `isAutoGenerated` | boolean | `true` for ASR (auto) captions, `false` for a manual track. |
| `availableLanguages` | array | Every caption track on the video: `{ languageCode, name, autoGenerated }`. |
| `sourceUrls` | array | Every input URL that produced this video (a video reached from two playlists lists both). |
| `sourcePosition` | number | null | Zero-based position within its expanded playlist/channel; `null` for direct video URLs. |
| `segments` | array | Raw caption cues in order: `{ startMs, durMs, text }`. |
| `chapters` | array | **The RAG wedge:** `{ title, startMs, endMs, paragraphs: [{ startMs, endMs, text }] }`. Always present; one `null`-title group when the video has no chapters. |
| `plainText` | string | null | Full transcript as text. Present only when `formats` includes `text`. |
| `srt`, `vtt` | string | null | Subtitle renderings. Present only when requested in `formats`. |

A **failed video** is a record with `errorReason` set and the transcript fields empty. It carries `errorReason`, `rawReason` (the raw upstream detail, for debugging), the offending `input` or `videoId`, and `sourceUrls`.

#### Dataset views

The dataset ships two views in the Apify Console:

- **Transcripts** — the transcript columns.
- **Errors** — `input`, `videoId`, `errorReason`, `rawReason`, `sourceUrls`.

> Views **project columns, they do not filter rows.** Both delivered transcripts and error items appear in **both** views (with the other view's columns blank). To separate them in your own code, split on `errorReason`: it is `null` on a delivered transcript and set on a failure.

See [`.actor/sample-output.json`](.actor/sample-output.json) for two full records from a real run (freeCodeCamp's *Learn Python* course and *Me at the zoo*).

***

### Failure modes

Every failure is a **free** error item — you are never billed for one. The `errorReason` is a stable machine-readable label; `rawReason` carries YouTube's own (locale-sensitive) detail.

| `errorReason` | Meaning | Billed? |
|---|---|---|
| `captionsDisabled` | The video has no caption tracks at all (captions turned off by the uploader). | No — free |
| `noCaptions` | Caption tracks exist, but **none match the languages you requested.** | No — free |
| `regionRestricted` | The video isn't available in the region the request came from. | No — free |
| `membersOnly` | The video is restricted to channel members. | No — free |
| `ageRestricted` | The video is age-gated and needs a signed-in account. | No — free |
| `videoUnavailable` | The video was removed, deleted, made private, or never existed (includes `[Private video]` / `[Deleted video]` playlist entries). | No — free |
| `notYetAvailable` | An upcoming premiere or offline live stream — no transcript yet. | No — free |
| `invalidUrl` | The input isn't a recognizable YouTube video, playlist, or channel URL. | No — free |
| `expansionFailed` | A playlist or channel URL couldn't be listed (its videos couldn't be enumerated). | No — free |
| `emptyTranscript` | A track was found but holds only music/noise markers — no spoken text. | No — free |
| `potRequired` | YouTube demanded a proof-of-origin token for this track; it can't be fetched on the current path. | No — free |
| `blocked` | The request was bot-checked or returned empty and kept failing after retries on fresh IPs. | No — free |
| `unknown` | An uncategorized failure — the raw upstream reason is captured in `rawReason` for debugging. | No — free |

***

### Language support

- **BCP-47 prefix matching.** A request for `en` matches `en`, `en-US`, and `en-GB` (but never `eng`); `pt-BR` matches only Brazilian Portuguese. Your `languages` list is tried in order — the first entry with any available match wins.
- **Manual beats auto.** Within a matched language, a human-made caption track is preferred over an auto-generated (ASR) one, and an exact tag beats a prefix match.
- **See what's on offer.** Every delivered item lists all real caption tracks in `availableLanguages` (manual + auto, never machine-translation targets), so you can widen your `languages` list or switch on `allLanguages` with full knowledge of what exists.

***

*Unofficial project. Uses only publicly available YouTube data. Not affiliated with YouTube or Google.*

> **Run it without configuring anything** — [Get the transcript of a YouTube video](https://apify.com/vonsensey/youtube-transcript-scraper/examples/transcript-of-a-youtube-video), a ready-made example you can start as-is or copy.

### Use cases

- **Build a RAG corpus.** Turn a channel, playlist or URL list into chunk-ready paragraphs with the source video and timestamp on every row — ready to embed.
- **Repurpose long video.** Pull the spoken text of a talk or podcast and turn it into a post, a newsletter or a clip list without watching it.
- **Search what was said.** Make a back catalogue greppable: find every mention of a product, a name or a claim across hundreds of videos.
- **Keep a corpus fresh.** Schedule it and re-runs skip videos already delivered, so you pay for new material only.

### Run it on a schedule

A one-off pull answers a question; a schedule answers it every day without you. Open **Schedules** in the Apify Console, point a cron at this Actor, and the dataset keeps filling on its own — no server, no cron box, no babysitting. Everything here is built to be re-run: you are billed per transcript delivered, so a scheduled run that finds nothing new costs nothing.

### FAQ

#### Do I need a YouTube API key?

No. No key, no login, no OAuth and no YouTube quota to manage — you give it a URL and it returns text.

#### Can I export transcripts to CSV, JSON or Excel?

Yes. Every run writes a dataset you can export in one click from the Console, or pull straight from the API in JSON, CSV, XLSX or JSONL.

#### What happens to a video with no captions?

It comes back as a free row that says so, rather than failing the run. You are never charged for a video that returned no transcript.

#### Can I transcribe a whole channel or playlist at once?

Yes — pass the channel or playlist URL and it walks the uploads for you. There are dedicated Actors in this suite for both.

***

Something wrong, or a field you need that is missing? Open an issue on the **Issues** tab — it is read and it gets fixed. If this saved you time, a rating on the Store page helps the next person find it.

# Actor input Schema

## `urls` (type: `array`):

Mixed list of YouTube URLs. Individual videos, playlists, channels, and @handles are all accepted — each playlist or channel URL is expanded into its videos automatically.

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

Ordered language preference for the transcript. BCP-47 tags or prefixes are accepted (e.g. "en" matches en, en-US, en-GB; "pt-BR" matches only Brazilian Portuguese). The first available match wins. Leave empty to take the video's default caption track.

## `allLanguages` (type: `boolean`):

Return every available caption track instead of just the best language match. Warning: this returns one dataset item per caption track, and each track is billed as one transcript.

## `includeShorts` (type: `boolean`):

When expanding channel URLs, also include Shorts. By default channel expansion covers long-form uploads only.

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

Maximum number of videos to take from each playlist or channel URL, applied per source URL independently. Direct video URLs are not affected.

## `includeChapters` (type: `boolean`):

Extract chapter markers and upload date for each video. Disabling this saves one request per video and leaves uploadDate null.

## `formats` (type: `array`):

Additional rendered formats to include on each dataset item. Structured JSON segments are always included.

## `onlyNewVideos` (type: `boolean`):

Skip videos already delivered by previous runs sharing the same state label. Useful for scheduled re-runs that should only pick up new uploads.

## `stateLabel` (type: `string`):

Names the persistent delivered-state store used by "Only new videos". Runs sharing a label share the same delivered-video memory.

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

Proxy settings for YouTube requests. Residential is the default because YouTube blocks most datacenter IPs for caption fetching (measured ~21% success on datacenter vs ~95% on residential). Keep residential unless you have your own proxy URLs.

## `spikeConfig` (type: `object`):

Internal: proxy-economics spike configuration. Leave empty.

## Actor input object example

```json
{
  "urls": [
    "https://www.youtube.com/watch?v=jNQXAC9IVRw"
  ],
  "allLanguages": false,
  "includeShorts": false,
  "maxVideosPerSource": 100,
  "includeChapters": true,
  "formats": [],
  "onlyNewVideos": false,
  "stateLabel": "default",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

No description

# 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 = {
    "urls": [
        "https://www.youtube.com/watch?v=jNQXAC9IVRw"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("vonsensey/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 = {
    "urls": ["https://www.youtube.com/watch?v=jNQXAC9IVRw"],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("vonsensey/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 '{
  "urls": [
    "https://www.youtube.com/watch?v=jNQXAC9IVRw"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call vonsensey/youtube-transcript-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,vonsensey/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/D06Mf3ECliKQJKOaC/builds/J2h7yDCASIZJothNT/openapi.json
