# YouTube Transcript Scraper — captions to Markdown (`fre.d/youtube-transcript`) Actor

Get YouTube video transcripts as clean Markdown and timed segments. Accepts video, channel and playlist URLs, picks your preferred caption language with automatic fallback, and returns title, channel, publish date, duration, word count and a per-video status. Failed videos are never charged.

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

## Pricing

from $2.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/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 Transcript Scraper — captions to Markdown

Turn YouTube videos into clean, LLM-ready transcripts. Pass video URLs, channel URLs or playlist URLs and get one record per video with the caption text as paragraph-formatted Markdown, the raw timed segments, title, channel, publish date, duration, the caption language that was used, word count and a per-video status. Built for RAG pipelines, summarizers, research agents and anyone who needs the words from a video without watching it.

### What you get

Each video produces exactly one dataset record:

| Field | Type | Description |
|---|---|---|
| `videoId` | string | null | 11-character YouTube video ID. `null` only when an input item could not be parsed or a channel/playlist could not be expanded |
| `url` | string | Canonical `https://www.youtube.com/watch?v=...` URL |
| `source` | string | The input item this record came from (useful when a channel or playlist was expanded) |
| `title` | string | null | Video title |
| `channelName` | string | null | Channel display name |
| `channelId` | string | null | Channel ID (`UC...`) |
| `publishedAt` | string | null | Publish date, ISO 8601 (UTC) |
| `durationSec` | integer | null | Video length in seconds |
| `language` | string | null | Language code of the caption track that was used, e.g. `en`, `en-US`, `ja` |
| `isAutoGenerated` | boolean | null | `true` when the track is YouTube's automatic speech recognition |
| `availableLanguages` | string\[] | All caption tracks the video offered, e.g. `["en", "en (auto)", "ja"]` |
| `transcript` | string | null | Paragraph-formatted transcript (Markdown). `null` when `outputFormat` is `segments` |
| `segments` | `{ start, dur, text }[]` | Timed caption segments in seconds. Empty when `outputFormat` is `markdown` |
| `wordCount` | integer | Words in the transcript (whitespace-separated tokens; Japanese, Chinese and Korean characters are counted individually) |
| `fetchedAt` | string | ISO 8601 timestamp of the fetch |
| `status` | `ok` | `failed` | `skipped` | `failed` = no captions, unavailable video or fetch error (see `error`); `skipped` = spending limit reached |
| `error` | string | Present only when `status` is not `ok` |

The run always finishes as **SUCCEEDED**, even when some videos have no captions or do not exist. Check `status` per record instead of the run status, so one private video never breaks your pipeline.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `videoUrls` | string\[] | required | Video URLs (`watch?v=`, `youtu.be/`, `/shorts/`, `/embed/`, `/live/`), bare 11-character video IDs, channel URLs (`/@handle`, `/channel/UC...`, `/c/...`, `/user/...`) or playlist URLs (`playlist?list=...`). Mixed lists are fine |
| `languages` | string\[] | `["en"]` | Preferred caption languages in order. See *Language fallback* below |
| `outputFormat` | `markdown` | `segments` | `both` | `both` | Which transcript fields to include |
| `includeTimestamps` | boolean | `false` | Prefix each paragraph of `transcript` with its start time, e.g. `[12:34]` |
| `maxVideosPerChannel` | integer | `50` | Upper bound on videos taken from each channel or playlist (1–1000). Channels are expanded newest first |
| `maxConcurrency` | integer | `5` | Videos processed in parallel (1–10) |
| `proxyConfiguration` | object | Apify residential proxy | YouTube blocks most datacenter IP ranges, so residential proxies are the default. You can switch to datacenter or custom proxies, but expect more `failed` records |

Example input:

```json
{
  "videoUrls": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "https://www.youtube.com/@apify",
    "https://www.youtube.com/playlist?list=PLxxxxxxxxxxxxxxxxxx"
  ],
  "languages": ["en", "ja"],
  "outputFormat": "both",
  "includeTimestamps": true,
  "maxVideosPerChannel": 20
}
```

### Output example

```json
{
  "videoId": "jNQXAC9IVRw",
  "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
  "source": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
  "title": "Me at the zoo",
  "channelName": "jawed",
  "channelId": "UC4QobU6STFB0P71PMvOGN5A",
  "publishedAt": "2005-04-24T03:31:52.000Z",
  "durationSec": 19,
  "language": "en",
  "isAutoGenerated": false,
  "availableLanguages": ["en", "de"],
  "transcript": "[00:01] All right, so here we are, in front of the elephants the cool thing about these guys is that they have really... really really long trunks and that's cool (baaaaaaaaaaahhh!!) and that's pretty much all there is to say",
  "segments": [
    { "start": 1.2, "dur": 2.16, "text": "All right, so here we are, in front of the elephants" },
    { "start": 5.318, "dur": 2.656, "text": "the cool thing about these guys is that they have really..." }
  ],
  "wordCount": 39,
  "fetchedAt": "2026-10-15T05:12:40.118Z",
  "status": "ok"
}
```

A video without captions looks like this and is **not charged**:

```json
{
  "videoId": "a9LDPn-MO4I",
  "url": "https://www.youtube.com/watch?v=a9LDPn-MO4I",
  "source": "https://www.youtube.com/watch?v=a9LDPn-MO4I",
  "status": "failed",
  "error": "No caption tracks available for this video",
  "title": null,
  "transcript": null,
  "segments": [],
  "wordCount": 0,
  "fetchedAt": "2026-10-15T05:12:40.118Z"
}
```

Other typical `error` values: `Video not available (ERROR): This video is unavailable`, `Video not available (LOGIN_REQUIRED): This video may be inappropriate for some users.` (age-restricted), `Playlist or channel unavailable: The playlist does not exist.`, `Not a YouTube URL`.

### Language fallback

For every video the Actor looks at the caption tracks YouTube offers and picks one in this order:

1. For each language in `languages`, in order: a **manually created** track in that language, then an **auto-generated** track in that language. `en` also matches regional variants such as `en-US` or `en-GB`; an exact match wins over a variant.
2. If none of the preferred languages exists: the first manual track in any language.
3. Finally: the first auto-generated track in any language.

The chosen track is reported in `language` and `isAutoGenerated`, and every track that was on offer is listed in `availableLanguages`, so you can decide whether the result is good enough for your use case. YouTube sometimes exposes auto-generated tracks in many languages for the same video and sometimes only in the original language; the list can vary between requests.

Transcripts are returned as published by YouTube: the Actor does not translate, summarize or correct them. Auto-generated tracks usually contain recognition errors and, for older videos, no punctuation.

### Pricing

Pay per event. You are charged only for videos that return `status: "ok"`.

| Event | Price | When |
|---|---|---|
| `transcript-fetched` | $0.002 per video | A caption track was fetched and converted |

Videos without captions, unavailable or private videos, invalid URLs, and the expansion of channels and playlists are free. Example: transcribing a 200-video channel where 180 videos have captions costs $0.36. You can cap spending per run with the *Maximum total charge* setting in Apify Console or the `maxTotalChargeUsd` API parameter; when the cap is reached, remaining videos are returned with `status: "skipped"`.

### Usage

#### Apify Console

Open the Actor, paste video, channel or playlist URLs into *Video, channel or playlist URLs*, click *Start*, and download the dataset as JSON, CSV or Excel. The *Overview* tab shows one row per video with status, language and word count.

#### JavaScript (apify-client)

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });

const run = await client.actor('fre.d/youtube-transcript').call({
  videoUrls: ['https://www.youtube.com/watch?v=dQw4w9WgXcQ', 'https://www.youtube.com/@apify'],
  languages: ['en'],
  outputFormat: 'markdown',
  maxVideosPerChannel: 10,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const video of items) {
  if (video.status === 'ok') console.log(video.title, video.language, video.wordCount);
  else console.warn(video.url, video.error);
}
```

#### Python (apify-client)

```python
import os
from apify_client import ApifyClient

client = ApifyClient(token=os.environ["APIFY_TOKEN"])

run = client.actor("fre.d/youtube-transcript").call(run_input={
    "videoUrls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],
    "languages": ["en", "ja"],
    "includeTimestamps": True,
})

for video in client.dataset(run["defaultDatasetId"]).iterate_items():
    if video["status"] == "ok":
        print(video["title"], video["transcript"][:200])
    else:
        print("failed:", video["url"], video["error"])
```

#### AI agents via MCP

Add the Apify MCP server to your agent (Claude Desktop, Cursor, LangGraph, etc.) and allow this Actor:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com/?tools=fre.d/youtube-transcript",
      "headers": { "Authorization": "Bearer <APIFY_TOKEN>" }
    }
  }
}
```

The agent can then call the tool with `{"videoUrls": ["https://www.youtube.com/watch?v=..."]}` and read `transcript` directly from the result. Because `videoUrls` accepts channels and playlists, an agent can pull a whole series in one tool call.

#### HTTP API

```bash
curl -X POST "https://api.apify.com/v2/acts/fre.d~youtube-transcript/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"videoUrls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"], "outputFormat": "markdown"}'
```

### How it works

1. Each input item is classified as a video, channel or playlist. Channels are resolved to their uploads list; playlists and channels are paged through until `maxVideosPerChannel` is reached. Duplicate videos across inputs are fetched once.
2. For each video the Actor asks YouTube's internal player endpoint for the video details and the list of caption tracks, picks a track using the language fallback above, and downloads that track through the same proxy session (caption URLs are bound to the IP that requested them).
3. Caption cues are cleaned (HTML entities decoded, whitespace collapsed) and grouped into paragraphs: a new paragraph starts after a pause of more than 2.5 seconds, at a sentence end once a paragraph is longer than 30 seconds, or at 60 seconds / 150 words at the latest. Japanese, Chinese and Korean cues are joined without inserted spaces.
4. The publish date is looked up in a second lightweight call; if it fails, `publishedAt` is `null` and the record is still returned.

Requests go through Apify residential proxies by default because YouTube refuses or empties caption responses for most datacenter IP ranges. Each parallel worker keeps its own proxy session; a session that gets a bot check or an empty caption body is discarded and the video is retried on a fresh one.

### Limitations

- Only videos that have captions (manual or auto-generated) can be transcribed. Live streams return `failed` until the stream has ended and YouTube has processed captions.
- Private, deleted, members-only and age-restricted videos return `failed`. The Actor never signs in and never uses cookies.
- Channel expansion takes videos from the channel's uploads list (newest first), which includes Shorts and past live streams. There is no filter by date or type yet.
- Auto-generated captions reflect YouTube's speech recognition quality. The Actor does not correct, translate or summarize them.
- `wordCount` is approximate for mixed-script text.
- This Actor relies on YouTube's undocumented internal endpoints, which change from time to time. If you see a sudden rise in `failed` records, check the Actor's *Issues* tab; fixes are usually released within a few days.

### Legal and responsible use

This Actor is **not affiliated with or endorsed by YouTube or Google** and does not use the official YouTube Data API. It retrieves the caption tracks that YouTube serves publicly to viewers.

Captions and transcripts are the copyrighted work of the video owner (or of YouTube for auto-generated tracks under the owner's content). You are responsible for how you store, redistribute or otherwise use the text you retrieve, including compliance with YouTube's Terms of Service and applicable copyright and data-protection law. Fetching a transcript to summarize or analyze a video for yourself is a different matter from republishing it; when in doubt, link to the video instead of redistributing the text.

### Support

Open an issue on the Actor's *Issues* tab in Apify Store, or email the maintainer listed on the Actor page. Feature requests (date filters for channels, translation via YouTube's translated tracks, SRT/VTT output, chunking for embeddings) are welcome.

# Changelog

This Actor's version history is a separate document: https://apify.com/fre.d/youtube-transcript/changelog.md

# Actor input Schema

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

YouTube video URLs (watch, youtu.be, shorts, embed, live), bare 11-character video IDs, channel URLs (/@handle, /channel/UC..., /c/..., /user/...) or playlist URLs (playlist?list=...). Channels and playlists are expanded into their videos (newest first, up to maxVideosPerChannel). One output record is produced per video.

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

BCP-47 language codes in order of preference, e.g. \["en", "ja"]. For each language the Actor first looks for a manually created caption track, then for an auto-generated one. If none of the preferred languages exists, it falls back to any manual track and finally to any auto-generated track. The chosen track is reported in the 'language' and 'isAutoGenerated' fields.

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

'markdown' returns the paragraph-formatted transcript only; 'segments' returns the raw timed segments only; 'both' returns both fields.

## `includeTimestamps` (type: `boolean`):

Prefix every paragraph of the Markdown transcript with its start time, e.g. \[12:34]. Segments always carry exact start times regardless of this setting.

## `maxVideosPerChannel` (type: `integer`):

Upper bound on how many videos are taken from each channel or playlist URL (newest first for channels). Does not affect video URLs passed directly.

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

How many videos are processed in parallel. Each parallel worker uses its own proxy session.

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

YouTube blocks most datacenter IP ranges, so Apify residential proxies are used by default. You can switch to datacenter proxies, your own proxies, or no proxy, but expect more 'failed' records in that case.

## Actor input object example

```json
{
  "videoUrls": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "https://www.youtube.com/watch?v=jNQXAC9IVRw"
  ],
  "languages": [
    "en"
  ],
  "outputFormat": "both",
  "includeTimestamps": false,
  "maxVideosPerChannel": 50,
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

All video records as JSON, including transcript text and timed segments.

## `transcriptsMarkdown` (type: `string`):

Same records with only videoId, url, title, language and transcript fields.

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

Flat CSV with videoId, url, status, title, channelName, language, isAutoGenerated, wordCount and error.

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

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

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

```

## MCP server setup

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

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/b9dq7ZoSzY2HtK8EB/builds/KFcUY4CdXGuFKVkB1/openapi.json
