# YouTube Transcript Scraper (`serentity/youtube-transcript-scraper`) Actor

Get the transcript of any YouTube video by URL or ID. Returns plain text and timed segments in any available caption language, with SRT and VTT output, auto vs manual caption flags and chapters. No API key needed.

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

## Pricing

from $1.40 / 1,000 transcript (one per video result)s

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 Transcript Scraper

Get the transcript of any YouTube video as plain text and timed segments. Give it video URLs or IDs, pick a language, and get back the captions the video actually has, with the title, channel, duration, chapters and a flag telling you whether the captions were written by the creator or generated by YouTube. No API key, no cookies, no browser.

Built for AI pipelines, research and content tools: array input, one item per video, clean error codes instead of failed runs, and SRT or WebVTT when you need subtitle files.

### What you get

For every video, one dataset item with:

| Field | Description |
|---|---|
| `videoId`, `url`, `title`, `channelName`, `channelId`, `durationSeconds`, `viewCount` | Video metadata |
| `text` | The full transcript as one string |
| `segments` | Timed lines: `start`, `duration`, `end` in seconds and `text` |
| `language`, `languageName`, `captionKind` | Which track was returned; `captionKind` is `manual` (creator-uploaded) or `auto` (YouTube speech recognition) |
| `availableLanguages`, `translatableTo` | Every caption track the video offers, and the languages YouTube can auto-translate it into |
| `chapters` | Chapters parsed from the description timestamps, when the creator added them |
| `wordCount`, `segmentCount` | Sizes |
| `srt`, `vtt` | Subtitle files as strings, when you switch them on |
| `error`, `errorMessage` | Present only on items that could not be fetched, see below |

### Sample output

```json
{
  "videoId": "jNQXAC9IVRw",
  "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
  "title": "Me at the zoo",
  "channelName": "jawed",
  "channelId": "UC4QobU6STFB0P71PMvOGN5A",
  "durationSeconds": 19,
  "viewCount": 447816985,
  "language": "en",
  "languageName": "English",
  "captionKind": "manual",
  "text": "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 and that's pretty much all there is to say",
  "wordCount": 39,
  "segmentCount": 6,
  "segments": [
    { "start": 1.2, "duration": 2.16, "end": 3.36, "text": "All right, so here we are, in front of the elephants" },
    { "start": 3.36, "duration": 2.4, "end": 5.76, "text": "the cool thing about these guys" }
  ],
  "chapters": [
    { "title": "Intro", "startSeconds": 0 },
    { "title": "The cool thing", "startSeconds": 5 },
    { "title": "End", "startSeconds": 17 }
  ],
  "availableLanguages": [
    { "code": "en", "name": "English", "kind": "manual" },
    { "code": "de", "name": "German", "kind": "manual" }
  ],
  "translatableTo": ["ar", "bs", "de", "es", "fr", "ja"],
  "source": "android",
  "fetchedAt": "2026-11-05T14:47:26.123Z",
  "input": "https://www.youtube.com/watch?v=jNQXAC9IVRw"
}
```

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `videoUrls` | array of strings | required | Video URLs or 11-character IDs. Watch, `youtu.be`, Shorts, embed, live and mobile URLs all work. Duplicates are fetched once. |
| `languages` | array of strings | `["en"]` | Preferred caption languages in order. `en` also matches `en-US` and `en-GB`. |
| `preferManual` | boolean | `true` | Prefer creator-uploaded captions over auto-generated ones in the same language. |
| `fallbackToAnyLanguage` | boolean | `true` | If none of the preferred languages exists, return the video's default track instead of an error. |
| `translateTo` | string | none | Ask YouTube to auto-translate the chosen track into this language. Best effort, see below. |
| `includeSegments` | boolean | `true` | Include the timed `segments` array. Turn off for smaller items. |
| `includeSrt`, `includeVtt` | boolean | `false` | Add subtitle-file strings. |
| `maxConcurrency` | integer | `5` | Videos fetched in parallel (1 to 20). |
| `proxyConfiguration` | object | Apify datacenter proxy | Proxy for all requests. |
| `residentialFallback` | boolean | `true` | If YouTube blocks the datacenter proxy for a video, retry that video through residential proxies. Only used on a detected block. |

Minimal input:

```json
{ "videoUrls": ["https://www.youtube.com/watch?v=jNQXAC9IVRw", "dQw4w9WgXcQ"] }
```

### Languages and translation

- The actor returns what the video has. Most videos have an auto-generated track in the spoken language; many also have creator-uploaded tracks in several languages. `availableLanguages` lists them all so you can make a second request for another language.
- `languages` is a preference list. With `["bs", "hr", "en"]` you get Bosnian if it exists, otherwise Croatian, otherwise English, otherwise the default track (or a `LANGUAGE_NOT_AVAILABLE` item if you set `fallbackToAnyLanguage` to false).
- `translateTo` requests YouTube's own machine translation of the chosen track. YouTube refuses this for some tracks and rate-limits it aggressively, so treat it as a bonus: when it is refused, you still get the original transcript, plus a `translationError` field saying why. `translatableTo` tells you which languages YouTube offers for that video.

### Errors

The run does not fail because one video is broken. Each problem video is a normal dataset item with an `error` code and an `errorMessage`, so your pipeline can branch on it:

| `error` | Meaning | Charged? |
|---|---|---|
| `INVALID_INPUT` | The string is not a YouTube video URL or ID | no |
| `VIDEO_UNAVAILABLE` | Deleted, private, region-locked or never existed | yes |
| `NO_CAPTIONS` | The video has no caption track at all (live streams, some music videos, captions disabled by the creator) | yes |
| `LANGUAGE_NOT_AVAILABLE` | None of your languages exists and fallback is off; the message lists what the video offers | yes |
| `LOGIN_REQUIRED` | YouTube only serves this video to signed-in users | yes |
| `BLOCKED` | YouTube refused every retrieval path for this video from every proxy tried | no |
| `FETCH_FAILED` | Network or parsing failure on every path | no |

Items with metadata (title, available languages) are filled in even when the transcript itself could not be returned, so `LANGUAGE_NOT_AVAILABLE` still tells you which languages to ask for.

### Pricing

Pay per event: you pay a small fixed amount per video result (successful transcript, or one of the charged error items above) plus Apify platform usage for the run. Our-side failures (`BLOCKED`, `FETCH_FAILED`) and invalid inputs are never charged. Set a maximum total charge on the run and the actor stops cleanly when it is reached, with the unprocessed count in the status message.

Typical cost: a run of 100 videos takes about a minute on 512 MB of memory and transfers roughly 0.1 MB per video.

### Reliability

YouTube changed caption delivery in 2025 and 2026: the URLs most scrapers use now return HTTP 200 with an empty body unless a proof-of-origin token is attached, and datacenter IP ranges are throttled. This actor:

- fetches captions through three independent paths (two mobile InnerTube clients, then the web player with a minted proof-of-origin token) and moves to the next one on any block or empty response;
- checks every response at body level, so an empty 200 is treated as a failure, never saved as an empty transcript;
- retries a blocked video once through residential proxies when `residentialFallback` is on;
- is tested daily with the default input.

`source` on each item tells you which path produced it, which is useful when reporting an issue.

### Limitations

- Live streams in progress have no caption track; the recording usually gets one a few hours after the stream ends.
- Videos with captions disabled by the creator, and most music videos, return `NO_CAPTIONS`.
- Private, members-only and age-gated videos that require a signed-in account are not supported; the actor never uses cookies.
- Transcript timing comes from YouTube's caption data. Auto-generated captions contain YouTube's own markers such as `[music]` or `>>` speaker changes.
- Chapters are parsed from description timestamps; videos whose chapters were set only in YouTube Studio without timestamps in the description return an empty array.

### Using it from code

Apify API, Node:

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('serentity/youtube-transcript-scraper').call({
    videoUrls: ['https://www.youtube.com/watch?v=jNQXAC9IVRw'],
    languages: ['en'],
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items[0].text);
```

Python:

```python
from apify_client import ApifyClient
client = ApifyClient("YOUR_TOKEN")
run = client.actor("serentity/youtube-transcript-scraper").call(run_input={"videoUrls": ["jNQXAC9IVRw"]})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["title"], item.get("wordCount"), item.get("error"))
```

The actor also works as a tool through the Apify MCP server: give an agent the list of URLs and it gets back the transcript text and segments in one call.

### Legal

Captions are public data that YouTube serves to any viewer. You are responsible for using transcripts in line with YouTube's terms of service and the copyright of the video's creator. The actor does not download video or audio, does not sign in, and does not collect personal data beyond the public channel name and ID shown on the video page.

# Actor input Schema

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

YouTube video URLs or 11-character video IDs. Accepts watch, youtu.be, Shorts, embed and live URLs. One transcript is returned per video.

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

Caption language codes in order of preference, for example en, en-US, de, es. The first language the video has is used. Leave empty to take the video's default track.

## `preferManual` (type: `boolean`):

When a language has both creator-uploaded and auto-generated captions, use the creator-uploaded ones.

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

If none of the preferred languages exists, return the video's default caption track instead of a LANGUAGE_NOT_AVAILABLE error.

## `translateTo` (type: `string`):

Optional. A language code (for example bs, de, ja) to request YouTube's automatic translation of the chosen track. Best effort: YouTube does not offer translation for every track or from every network, so the original transcript is returned with a translationError field when translation is refused.

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

Include the segments array with start time, duration and text of every caption line.

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

Add the transcript as an SRT subtitle string in the srt field.

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

Add the transcript as a WebVTT subtitle string in the vtt field.

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

How many videos are fetched at the same time.

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

Proxy used for the requests. Apify datacenter proxies are used by default.

## `residentialFallback` (type: `boolean`):

If YouTube blocks the datacenter proxy for a video, retry that video through Apify residential proxies. Residential traffic is small (about 0.1 to 0.3 MB per video) and is only used when a block is detected.

## Actor input object example

```json
{
  "videoUrls": [
    "https://www.youtube.com/watch?v=jNQXAC9IVRw",
    "dQw4w9WgXcQ"
  ],
  "languages": [
    "en"
  ],
  "preferManual": true,
  "fallbackToAnyLanguage": true,
  "includeSegments": true,
  "includeSrt": false,
  "includeVtt": false,
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "residentialFallback": true
}
```

# Actor output Schema

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

// Run the Actor and wait for it to finish
const run = await client.actor("serentity/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=jNQXAC9IVRw",
        "dQw4w9WgXcQ",
    ],
    "languages": ["en"],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("serentity/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=jNQXAC9IVRw",
    "dQw4w9WgXcQ"
  ],
  "languages": [
    "en"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call serentity/youtube-transcript-scraper --silent --output-dataset

```

## MCP server setup

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