# YouTube Transcript & Subtitles Scraper (`wheaten_steelpan/youtube-transcript-scraper`) Actor

Get YouTube transcripts and subtitles from video, channel and playlist links: plain text, timestamped segments, SRT and VTT, in the video's own language or the one you pick. Every caption track listed. Charged per transcript; videos without captions are free.

- **URL**: https://apify.com/wheaten_steelpan/youtube-transcript-scraper.md
- **Developed by:** [Vanja V](https://apify.com/wheaten_steelpan) (community)
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$4.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.

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 & Subtitles Scraper

Get the transcript of any YouTube video as plain text, timestamped segments, SRT or WebVTT
subtitles. Paste video links, channel links, playlist links, or any mix, and get one result row per
video with its title, channel, publish date and length.

**You pay $0.004 per transcript delivered, and only for videos that returned one.** There is no
start fee, and proxy and compute costs are included. A video without captions, a private or deleted
video, or a video without a track in the language you asked for comes back with the reason in its
row and costs nothing.

You need no YouTube API key, no Google account and no cookies.

### What you pay for

**$0.004 per transcript** ($4 per 1,000), whatever the video's length.

| outcome | `status` | charged |
|---|---|---|
| a transcript was returned | `ok` | **yes**, once per video |
| the video has no captions, not even automatic ones | `no_captions` | no |
| you asked for a language the video has no track in | `language_not_available` | no |
| private, deleted, members-only, age-restricted or region-blocked | `unavailable` | no |
| YouTube kept answering with a bot check after several fresh connections | `blocked` | no |

Every row that is not `ok` has an `error` field that says what happened in one plain sentence. The
other videos in the run carry on. If you set a maximum cost per run, the Actor stops when the next
transcript would go over it and says so in the run's status message.

### What you get

One dataset row per video:

```json
{
  "videoId": "Jf75I9LKhvg",
  "url": "https://www.youtube.com/watch?v=Jf75I9LKhvg",
  "title": "How To Cook The Perfect Rice | Gordon Ramsay",
  "channelName": "Gordon Ramsay",
  "channelId": "UCIEv3lZ_tNXHzL3ox-_uUGQ",
  "publishDate": "2013-12-05T08:00:00-08:00",
  "lengthSeconds": 140,
  "status": "ok",
  "requestedLanguage": "original",
  "language": "en",
  "trackKind": "auto",
  "availableTracks": [
    { "language": "en", "name": "English (auto-generated)", "kind": "auto" }
  ],
  "transcript": "basmati rice the king of all rice is light fluffy delicious …",
  "segments": [
    { "start": 0.08, "duration": 2.36, "text": "basmati rice the king of all rice is" }
  ],
  "srt": "1\n00:00:00,080 --> 00:00:02,440\nbasmati rice the king of all rice is\n\n…",
  "vtt": "WEBVTT\n\n00:00:00.080 --> 00:00:02.440\nbasmati rice the king of all rice is\n\n…",
  "error": null
}
```

- `transcript` is always filled on `ok` rows. `segments`, `srt` and `vtt` are filled when you pick
  them in `outputFormats`, and `null` otherwise, so every row has the same fields.
- `trackKind` is `manual` for captions the uploader provided and `auto` for YouTube's automatic ones.
- `availableTracks` lists every caption track the video has, on every row where YouTube returned the
  video, so you can see what else you could ask for.
- `publishDate` and `lengthSeconds` come straight from YouTube.

The run opens on a **Transcripts** table (title, channel, publish date, status, language, track
kind, transcript, link, error). The full JSON has everything.

### Input

| field | what it does |
|---|---|
| `videoUrls` | Video links (watch, youtu.be, Shorts, live, embed) or bare 11-character IDs. |
| `channelUrls` | Channel links (`@handle`, `/channel/UC…`, `/c/`, `/user/`). Takes the newest uploads, Shorts and past live streams included. |
| `playlistUrls` | Playlist links or IDs, in playlist order. |
| `maxVideosPerChannel` | How many videos to take from each channel or playlist (default 20). |
| `language` | `original` (default) or a language code such as `en`, `es`, `de`, `ja`. |
| `onlyNewSinceLastRun` | For scheduled runs: skip channel and playlist videos an earlier run already returned. |
| `outputFormats` | Any of `segments` (default), `srt`, `vtt`. Plain text is always included. |

Duplicates are removed, so a video that is both in a playlist and in your video links is fetched and
charged once.

### Languages

With `language` set to `original`, each video comes back in its own spoken language: captions the
uploader provided first, then YouTube's automatic ones. Dubbed videos come back in the language of
the original audio. In our test set of 41 captioned videos in 5 languages (English, Spanish, German,
Japanese, Hindi), all 41 came back in the right language.

With a language code, you get that track if the video has one, uploaded or automatic. If it has
none, the row is `language_not_available` with the tracks it does have in `availableTracks`, and it
is not charged. The Actor does not machine-translate.

### Channels and scheduled runs

Give a channel link and a schedule, turn on `onlyNewSinceLastRun`, and each run returns only the
videos published since the last one. A run with nothing new returns no rows and costs nothing. The
list of videos already returned is kept in a key-value store named
`youtube-transcript-scraper-seen` in your own Apify account; delete it to start over.

### Speed and reliability

Measured on the platform: 63 videos from links, a channel and a playlist in 80 seconds,
58 of them on the first try and the other 5 on the second. Requests go through residential proxies, which are included in the price. When
YouTube shows a bot check, the Actor retries that video on fresh connections before it gives up
with `blocked`.

### Use it from Make and n8n

The Actor works with the Apify modules in Make and the Apify node in n8n: run it, then read the
dataset. Every field has a fixed type (`lengthSeconds` is a number, `segments` an array of
`{start, duration, text}`), so you can map them directly.

### What this cannot do

- Videos without any captions come back as `no_captions`. The Actor reads YouTube's caption tracks
  and does not transcribe audio.
- No translation: you get the tracks a video has.
- Private, members-only and age-restricted videos need a signed-in account, so they come back as
  `unavailable`.
- No comments, likes or view counts.

### Pricing

| | |
|---|---|
| per transcript (`ok` row) | $0.004 |
| per 1,000 transcripts | $4.00 |
| start fee | none |
| proxy and compute | included |
| rows that are not `ok` | free |

# Actor input Schema

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

YouTube video links (watch, youtu.be, Shorts, live, embed) or bare 11-character video IDs. Each video becomes one row. Duplicates are removed.

## `channelUrls` (type: `array`):

Channel links (@handle, /channel/UC…, /c/ or /user/). The newest uploads of each channel are fetched, Shorts and past live streams included.

## `playlistUrls` (type: `array`):

Playlist links (any link with list=…) or playlist IDs.

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

How many videos to take from each channel (newest first) or playlist (in playlist order).

## `language` (type: `string`):

"original" returns each video in its own spoken language (uploaded captions first, then automatic ones). Or enter a language code such as en, es, de, fr or ja: videos with no track in that language come back as language_not_available, with their tracks listed, and are not charged.

## `onlyNewSinceLastRun` (type: `boolean`):

For scheduled runs on channels and playlists: skip videos an earlier run of yours already returned. Video links are always fetched.

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

Plain text is always in the transcript field. Add timestamped segments, SRT subtitles or WebVTT subtitles; fields you leave out are null.

## Actor input object example

```json
{
  "videoUrls": [
    "https://www.youtube.com/watch?v=Jf75I9LKhvg"
  ],
  "maxVideosPerChannel": 20,
  "language": "original",
  "onlyNewSinceLastRun": false,
  "outputFormats": [
    "segments"
  ]
}
```

# Actor output Schema

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

One row per video with title, channel, publish date, status, language and the plain-text transcript.

## `data` (type: `string`):

Every field of every row, including caption tracks, segments, SRT and VTT.

# 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=Jf75I9LKhvg"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("wheaten_steelpan/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=Jf75I9LKhvg"] }

# Run the Actor and wait for it to finish
run = client.actor("wheaten_steelpan/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=Jf75I9LKhvg"
  ]
}' |
apify call wheaten_steelpan/youtube-transcript-scraper --silent --output-dataset

```

## MCP server setup

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