# YouTube Transcript & Subtitles API: Bulk, Channels, Playlists (`nawfel_bc/youtube-transcript-api`) Actor

Get YouTube transcripts and subtitles (SRT/VTT) in bulk, from videos, channels or playlists. $5 per 1,000 on paid plans, and you only pay for successful transcripts.

- **URL**: https://apify.com/nawfel_bc/youtube-transcript-api.md
- **Developed by:** [Nawfel](https://apify.com/nawfel_bc) (community)
- **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/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

Get YouTube transcripts and subtitles (SRT/VTT) in bulk: one video, a list, a whole channel, or a playlist, as clean JSON with timestamps.
Compatible with the common `videoUrl` + `targetLanguage` input and `{"data": [{start, dur, text}]}` output, so a single-video pipeline can switch by changing only the Actor ID.
**$5 per 1,000 transcripts on paid plans, and you only pay for successful transcripts.** Videos without captions are free.

### What you get

- **Transcripts with timestamps**: every caption line with its start time and duration, plus the full text as one string.
- **Bulk, channels and playlists**: pass hundreds of video URLs, a channel (`@handle`), or a playlist in one run.
- **Subtitles**: standard **SRT** and **WebVTT** files, ready to upload or burn in.
- **AI-ready chunks**: transcript split into pieces of the size you choose, each with its time range, chapter, and a link that opens the video at that exact moment (`&t=123s`). Built for RAG pipelines and chatbots that cite their sources.
- **Chapters and video details**: title, channel, duration, view count, and the video's chapters ("Intro", "Pricing", ...) with timestamp links.
- **Find the moment**: give keywords ("pricing", "Bitcoin", a brand name) and get every moment they are said, with links.
- **Channel monitoring**: schedule the Actor on a channel and each run returns only the new videos' transcripts.
- **Markdown and paragraphs**: readable text grouped by pauses, with clickable timestamps, for blog posts and notes.
- **Reliable**: blocked requests are retried automatically on fresh connections, and region-locked videos are retried from other countries. Runs don't fail because one video has no captions.

### Pricing

| | Price |
|---|---|
| Successful transcript, paid Apify plans | **$5 per 1,000** ($0.005 each) |
| Successful transcript, free Apify plan | $10 per 1,000 |
| Video without captions, private, deleted, age-restricted... | **free** |
| Start fee, compute, proxies | **included** |

Chapters, chunks, SRT, VTT, Markdown, paragraphs and keyword search are included at no extra cost. The spending limit you set on a run is respected: the run stops cleanly when it is reached, and you are never charged above it.

### Quick start

**One video** (the classic input):

```json
{
  "videoUrl": "https://www.youtube.com/watch?v=8KkKuTCFvzI",
  "targetLanguage": "en"
}
```

**Many videos**: `videoUrl` and `videoUrls` accept watch URLs, `youtu.be` links, Shorts, embed and live URLs, or bare 11-character IDs. Duplicates are removed.

```json
{
  "videoUrls": [
    "https://www.youtube.com/watch?v=8KkKuTCFvzI",
    "https://youtu.be/dQw4w9WgXcQ",
    "https://www.youtube.com/shorts/BR06T1eJ3go"
  ],
  "targetLanguage": "en"
}
```

**A channel or a playlist** (newest videos first, up to `maxVideos`):

```json
{
  "channelUrl": "https://www.youtube.com/@TED",
  "playlistUrl": "https://www.youtube.com/playlist?list=PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "maxVideos": 100
}
```

**Subtitles and AI chunks**:

```json
{
  "videoUrls": ["https://www.youtube.com/watch?v=8KkKuTCFvzI"],
  "chunkSize": 1500,
  "extraFormats": ["srt", "vtt", "markdown", "paragraphs"]
}
```

**Find every moment a word is said** across a channel (`outputFormat: "none"` keeps the results small):

```json
{
  "channelUrl": "@veritasium",
  "maxVideos": 50,
  "searchKeywords": ["black hole", "quantum"],
  "outputFormat": "none"
}
```

**Monitor a channel**: create a Schedule (daily or weekly) with this input. Each run returns only videos it hasn't delivered before.

```json
{
  "channelUrl": "@mkbhd",
  "maxVideos": 20,
  "onlyNewVideos": true
}
```

### Input

| Field | Type | Description |
|---|---|---|
| `videoUrl` | string | One video URL or ID. |
| `targetLanguage` | string | Transcript language, ISO 639-1 (`en`, `es`, `pt`, `fr`, `de`, `ja`, ...). Default `en`. |
| `videoUrls` | array | Many video URLs or IDs. |
| `channelUrl` | string | `@handle`, channel URL, or `UC...` channel ID. |
| `playlistUrl` | string | Playlist URL or ID. |
| `maxVideos` | integer | Videos taken from the channel and from the playlist. Default 50. |
| `outputFormat` | string | `both` (default), `segments` (`data` only), `text` (`text` only), `none`. |
| `fallbackToAnyLanguage` | boolean | If the requested language isn't available, return the video's own language (flagged). Default `true`. |
| `searchKeywords` | array | Words or phrases to find; returns `matches`. |
| `chunkSize` | integer | Split into chunks of up to this many characters (e.g. 1000-2000). 0 = off. |
| `extraFormats` | array | Any of `srt`, `vtt`, `markdown`, `paragraphs`. |
| `onlyNewVideos` | boolean | Monitoring: skip videos delivered by earlier runs of the same input. |
| `monitorStateKey` | string | Optional name to keep several monitors on the same channel separate. |

Give at least one of `videoUrl`, `videoUrls`, `channelUrl`, `playlistUrl`.

### Output

One dataset item per video. `data` is first and has the same shape as the common single-video format; everything else is added alongside it.

```json
{
  "data": [
    { "start": "0.280", "dur": "4.440", "text": "Welcome back to the channel." },
    { "start": "4.720", "dur": "3.960", "text": "Today we're looking at pricing." }
  ],
  "videoId": "8KkKuTCFvzI",
  "videoUrl": "https://www.youtube.com/watch?v=8KkKuTCFvzI",
  "title": "Example video title",
  "channelName": "Example Channel",
  "channelId": "UCxxxxxxxxxxxxxxxxxxxxxx",
  "durationSeconds": 754,
  "viewCount": 182345,
  "chapters": [
    { "title": "Intro", "start": 0, "end": 95, "startTime": "0:00", "url": "https://www.youtube.com/watch?v=8KkKuTCFvzI&t=0s" },
    { "title": "Pricing", "start": 95, "end": 754, "startTime": "1:35", "url": "https://www.youtube.com/watch?v=8KkKuTCFvzI&t=95s" }
  ],
  "language": "en",
  "requestedLanguage": "en",
  "isAutoGenerated": false,
  "isLanguageFallback": false,
  "availableLanguages": ["en", "es"],
  "text": "Welcome back to the channel. Today we're looking at pricing. ...",
  "status": "success",
  "errorType": null,
  "errorMessage": null
}
```

With options, items also contain:

- `chunks`: `[{ index, start, end, startTime, url, chapter, text }]`
- `matches` and `matchCount`: `[{ keyword, start, startTime, url, chapter, context }]`
- `srt`, `vtt`, `markdown`: strings; `paragraphs`: `[{ start, startTime, url, text }]`

For very long videos (several hours) with many formats, the biggest formats are saved to the run's key-value store and the item links to them (`srtUrl`, `vttUrl`, ...) to stay within dataset size limits.

The dataset has three views: **Transcripts**, **Details** (video details and language handling), and **Moments** (one row per keyword match).

### Errors

Videos without a transcript are still listed, so you can see what happened, and are **never charged**:

| `errorType` | Meaning |
|---|---|
| `NO_CAPTIONS` | The video has no captions (manual or auto-generated). |
| `LANGUAGE_NOT_AVAILABLE` | No captions in `targetLanguage` and `fallbackToAnyLanguage` is off. `availableLanguages` lists what exists. |
| `VIDEO_UNAVAILABLE` | Private, deleted, or otherwise unavailable. |
| `AGE_RESTRICTED` | Age-restricted videos need a signed-in account, which this Actor never uses. |
| `MEMBERS_ONLY` | Channel-members-only video. |
| `LIVE_NOT_FINISHED` | A live stream or premiere that hasn't ended yet. |
| `GEO_RESTRICTED` | Not available in any country we tried. |
| `INVALID_URL` | Not a YouTube video URL or ID. |
| `BLOCKED_AFTER_RETRIES` | YouTube refused repeatedly. Rare; try again later. |

A run only fails if the input itself is invalid (for example, no video, channel or playlist given).

### Migrating from a single-video transcript Actor

If you call a transcript Actor with `{"videoUrl": ..., "targetLanguage": ...}` and read `items[0]["data"]`, change only the Actor ID:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_API_TOKEN>")
run = client.actor("nawfel_bc/youtube-transcript-api").call(run_input={
    "videoUrl": "https://www.youtube.com/watch?v=8KkKuTCFvzI",
    "targetLanguage": "en",
})
items = client.dataset(run["defaultDatasetId"]).list_items().items
segments = items[0]["data"] if items else None  # [{"start": "0.280", "dur": "4.440", "text": "..."}]
```

What stays the same with a classic single-video input (only `videoUrl`):

- The dataset item has `data` with `start` / `dur` / `text` as strings, in seconds with 3 decimals.
- The key-value store record `output` holds the same object; when there is no transcript it holds `{"success": false, "videoUrl", "targetLanguage", "error": {"code", "message", "retryable"}}`, with code `NO_CAPTION_AVAILABLE` for videos without captions.
- When there is no transcript, the dataset stays empty and the run still succeeds, with the reason in the status message.

One difference: **this Actor never machine-translates.** If the video has no captions in `targetLanguage`, you get the video's own language with `isLanguageFallback: true` (or `LANGUAGE_NOT_AVAILABLE` if you turn fallback off).

Then, to process many videos, send them all in one run with `videoUrls` instead of one run per video: it is faster and you avoid per-run overhead.

### Use cases

- **RAG and AI agents**: index transcripts with `chunkSize`, and let your assistant cite the exact moment with the chunk `url`.
- **Content repurposing**: turn videos into blog posts, newsletters and social posts with `markdown` or `paragraphs`.
- **Subtitles**: export `srt` or `vtt` for editing, accessibility, or re-uploading.
- **SEO**: publish transcripts next to embedded videos.
- **Research and monitoring**: track what competitors, creators or news channels say with `searchKeywords` and scheduled `onlyNewVideos` runs.

### Use from AI agents (MCP)

This Actor can be used as a tool through the [Apify MCP server](https://mcp.apify.com). An agent can call it with `{"videoUrl": "...", "outputFormat": "text"}` for a quick summary input, or with `chunkSize` and `searchKeywords` to retrieve and cite specific moments.

### FAQ

**Which videos work?** Public YouTube videos that have captions, manual or auto-generated, including Shorts and finished live streams.

**Can I get a transcript in another language?** Only if the video has captions in that language. Transcripts are never machine-translated. Use `availableLanguages` to see what exists.

**What about videos without captions?** They return `NO_CAPTIONS` and are free. Speech-to-text transcription is not included.

**Does a failed video fail the run?** No. Each video gets its own item with `status` and `errorType`.

**How fast is it?** Up to 20 videos are processed at once; a typical video takes 1 to 5 seconds.

**Where are chapters from?** From the timestamps in the video's description ("0:00 Intro"), using YouTube's rules: at least three, starting at 0:00. Videos without them return `chapters: []`.

**How does monitoring remember what it already delivered?** In a key-value store named `youtube-transcript-monitor` in your own Apify account. Videos whose captions aren't ready yet are retried on later runs for up to 7 days, and charged only when they succeed.

**Do I need a YouTube account or API key?** No.

### Limitations

- Only public videos; never private, members-only or age-restricted ones.
- One transcript language per run.
- Chapters only when the description lists them.

# Actor input Schema

## `videoUrl` (type: `string`):

One YouTube video: a watch URL, youtu.be link, Shorts link, embed link, or the bare 11-character video ID. Same field as the common single-video transcript format.

## `targetLanguage` (type: `string`):

Language of the transcript, as an ISO 639-1 code (e.g. en, es, pt, fr, de, ja). Default: en. If the video has no captions in this language, see fallbackToAnyLanguage. Transcripts are never machine-translated.

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

Many videos in one run: a list of YouTube video URLs or 11-character video IDs. Duplicates are removed. You can combine this with videoUrl, channelUrl and playlistUrl.

## `channelUrl` (type: `string`):

Get transcripts for a channel's latest videos (newest first, up to maxVideos). Accepts https://www.youtube.com/@handle, @handle, /channel/UC… URLs or IDs, and /c/… or /user/… URLs.

## `playlistUrl` (type: `string`):

Get transcripts for the videos of a playlist (up to maxVideos). Accepts a playlist URL (any URL with list=…) or a playlist ID.

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

Maximum number of videos taken from channelUrl and from playlistUrl. Does not limit videoUrl/videoUrls.

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

segments = timestamped segments in `data` (\[{start, dur, text}]); text = the whole transcript as one string in `text`; both = both fields; none = neither (useful with searchKeywords or chunks to keep results small).

## `searchKeywords` (type: `array`):

Words or phrases to find in each transcript (e.g. pricing, Bitcoin, a competitor's name). Each match is returned in `matches` with its time, a link that opens the video at that moment (\&t=123s), the chapter, and the surrounding sentence. Case- and accent-insensitive; phrases can span caption lines. Charged per transcript searched.

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

Also split each transcript into chunks of up to this many characters (e.g. 1000-2000 for RAG / embeddings). Each chunk has start/end times, its chapter, and a link that opens the video at that moment (\&t=123s). Chunks never split a caption line or cross a chapter. 0 = no chunks. No extra cost.

## `extraFormats` (type: `array`):

Also return the transcript as: srt / vtt (standard subtitle files), markdown (title, chapter headings, and paragraphs with clickable timestamps), paragraphs (readable paragraphs split at pauses, each with a timestamp link). No extra cost.

## `fallbackToAnyLanguage` (type: `boolean`):

If the video has no captions in targetLanguage, return the transcript in the video's own language instead (flagged with isLanguageFallback: true; charged as a transcript). If off, such videos return LANGUAGE_NOT_AVAILABLE with the list of availableLanguages, and are not charged.

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

For scheduled runs: skip videos this input already delivered in earlier runs, so each run returns only new videos' transcripts. Videos without captions yet (common for fresh uploads) are retried on later runs for up to 7 days and are not charged until they succeed. State is kept in a key-value store named youtube-transcript-monitor in your account.

## `monitorStateKey` (type: `string`):

Optional. Name for this monitor's memory, if you run several schedules on the same channel and want them tracked separately. By default it is derived from the channel/playlist/videos and language.

## Actor input object example

```json
{
  "videoUrl": "https://www.youtube.com/watch?v=8KkKuTCFvzI",
  "targetLanguage": "en",
  "maxVideos": 50,
  "outputFormat": "both",
  "chunkSize": 0,
  "extraFormats": [],
  "fallbackToAnyLanguage": true,
  "onlyNewVideos": false
}
```

# Actor output Schema

## `transcript` (type: `string`):

One item per video: timestamped segments in `data`, full text in `text`, plus title, language and status.

## `output` (type: `string`):

Single-video runs (videoUrl only): the same transcript object as the dataset item, or the error payload if there is no transcript.

## `summary` (type: `string`):

Counts of transcripts and errors by type for this run.

# 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 = {
    "videoUrl": "https://www.youtube.com/watch?v=8KkKuTCFvzI",
    "targetLanguage": "en"
};

// Run the Actor and wait for it to finish
const run = await client.actor("nawfel_bc/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 = {
    "videoUrl": "https://www.youtube.com/watch?v=8KkKuTCFvzI",
    "targetLanguage": "en",
}

# Run the Actor and wait for it to finish
run = client.actor("nawfel_bc/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 '{
  "videoUrl": "https://www.youtube.com/watch?v=8KkKuTCFvzI",
  "targetLanguage": "en"
}' |
apify call nawfel_bc/youtube-transcript-api --silent --output-dataset

```

## MCP server setup

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