# YouTube Transcript Scraper API - Captions and Subtitles (`neverempty/youtube-transcript-reliable`) Actor

Extracts YouTube transcripts (manual captions and auto-generated). Tells you whether a video is private, has no captions, or was blocked - instead of returning a blank row - and charges only for transcripts that actually contain text.

- **URL**: https://apify.com/neverempty/youtube-transcript-reliable.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Videos, AI, Agents
- **Stats:** 1 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.92 / 1,000 transcript returneds

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?

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 API - Captions and Subtitles

Get the transcript of any public YouTube video — manual captions and auto-generated ones — as clean text, or as timestamped lines.

**The difference is what happens when something goes wrong.** Most transcript scrapers return an empty result whether the video has no captions, the video is private, or YouTube blocked the request. You cannot tell which, so you cannot fix it. This one tells you, in plain words, and **only charges you for transcripts that actually contain text**.

### Measured, not claimed

A run of 15 videos on 2026-08-25, from the shipped build:

- **14 returned a transcript.** 83,647 characters total.
- **1 did not — and it said why: "この動画は非公開です" (this video is private).** Nobody can scrape that one. It was not silently dropped, and it was not charged.
- 48.7 seconds for all 15.

### What 180 videos actually returned

The 15-video run above is small, so on **2026-08-29** the same engine was pointed at the 15 newest uploads
from 12 channels — 180 videos in all, picked from each channel's public feed rather than by hand.

| Outcome | Videos | Share |
|---|---|---|
| Transcript returned | **163** | 90.6% |
| No caption track on the video at all | 15 | 8.3% |
| Video not playable (private, removed, region) | 2 | 1.1% |

Of the 163 transcripts, **120 were auto-generated and 43 were manual captions**. In other words, on a
representative slice of active channels, **three transcripts in four are machine transcription**, and any
claim that a scraper "gets the subtitles the creator wrote" is wrong three times out of four.

#### It varies enormously by channel

| Channel | Transcripts | Auto-generated | No caption track |
|---|---|---|---|
| Veritasium | 15 / 15 | 11 | 0 |
| Marques Brownlee | 15 / 15 | 3 | 0 |
| 3Blue1Brown | 15 / 15 | 8 | 0 |
| Linus Tech Tips | 15 / 15 | 15 | 0 |
| TED | 15 / 15 | 8 | 0 |
| Computerphile | 15 / 15 | 15 | 0 |
| Fireship | 15 / 15 | 15 | 0 |
| NASA | 12 / 15 | 1 | 1 |
| ThePrimeagen | 14 / 15 | 14 | 1 |
| HIKAKIN | 14 / 15 | 14 | 1 |
| Kurzgesagt – In a Nutshell | 11 / 15 | 9 | 4 |
| **Billie Eilish** | **7 / 15** | 7 | **8** |

Music channels are the outlier: **more than half of the Billie Eilish uploads had no caption track at all.**
If your pipeline assumes "public video ⇒ transcript", a music-heavy input list will look broken.

*(Channel names above are the titles YouTube itself returned for each handle. An earlier version of this
measurement read the wrong channel id from the page and attributed videos to the wrong channels; the numbers
here are from the corrected run.)*

### Asking for English does not guarantee English

The same 180-video run requested `["en"]` every time.

- **156 of 163 transcripts came back in English (95.7%).**
- **7 came back in another language** — Arabic on a TED upload, Dutch on a Computerphile upload, and
  Japanese, Spanish and Vietnamese on HIKAKIN uploads.
- In **none** of those 7 cases did the video have an English track that was passed over. When you get another
  language, it is because English was not there.

That is why every row carries `language`, `isAutoGenerated` and `availableLanguages`: so you can drop or
translate the odd row instead of quietly mixing languages into a dataset you thought was English.

### What you get

| Field | Example |
|---|---|
| `ok` | `true` / `false` — did this video produce a transcript |
| `videoId` / `url` | `aircAruvnKk` |
| `title` / `author` | `But what is a neural network?` / `3Blue1Brown` |
| `lengthSeconds` | `1132` |
| `language` | `en` |
| `isAutoGenerated` | `false` — manual captions are preferred when both exist |
| `availableLanguages` | `["en","es","fr","ja", ...]` |
| `segmentCount` | `286` |
| `text` | The whole transcript as one string |
| `segments` | `[{ "start": 12.4, "duration": 3.1, "text": "..." }, ...]` |
| `reason` | Why it failed, when `ok` is `false` |

### Why transcripts come back empty elsewhere

The caption URL published on a YouTube watch page returns **zero bytes** for every format — measured on every video tried. Anything built on that path silently produces nothing. This Actor does not use it. It reads the same endpoint the official mobile apps use, which still serves captions, and it requests only the fields it needs, so a lookup transfers about 19 KB instead of 228 KB.

That is also why it is cheap to run at volume.

### Honest failure reasons

When a video does not produce a transcript, you get one of these in `reason` — never a blank row:

- `この動画は非公開です` — private video
- `メンバー限定の動画です` — members-only
- `年齢制限のある動画です` — age-restricted
- `この地域では再生できない動画です` — geo-blocked
- `この動画に字幕がありません` — the video genuinely has no captions
- `ボット判定でブロック` — YouTube rate-limited the request; retried automatically with a fresh IP first

**A private video and a blocked request are not the same problem, and this Actor does not pretend they are.**

### Input

```json
{
  "videos": [
    "https://www.youtube.com/watch?v=aircAruvnKk",
    "https://youtu.be/dQw4w9WgXcQ",
    "M7lc1UVf-VE"
  ],
  "languages": ["en"],
  "includeTimestamps": true,
  "proxyMode": "auto"
}
```

Watch URLs, `youtu.be` links, Shorts URLs, embed URLs, and bare 11-character IDs all work.

| Field | Default | Meaning |
|---|---|---|
| `videos` | *(required)* | URLs or IDs |
| `languages` | `["en"]` | Preferred order. Manual captions win over auto-generated |
| `includeTimestamps` | `true` | Include per-line `segments` alongside the full `text` |
| `proxyMode` | `auto` | `auto` tries datacenter first (cheaper), falls back to residential when YouTube blocks |
| `maxRetries` | `3` | Retries with a fresh IP on a block or a network failure |

### Calling it from your own code

JavaScript:

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('neverempty/youtube-transcript-reliable').call({
    videos: ['https://www.youtube.com/watch?v=aircAruvnKk'],
    languages: ['en'],
    includeTimestamps: true,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const row of items) {
    if (!row.ok) { console.warn(row.videoId, 'skipped:', row.reason); continue; }
    console.log(row.title, '-', row.language, row.isAutoGenerated ? '(auto)' : '(manual)');
    console.log(row.text.slice(0, 200));
}
```

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("neverempty/youtube-transcript-reliable").call(run_input={
    "videos": ["https://www.youtube.com/watch?v=aircAruvnKk"],
    "languages": ["en"],
    "includeTimestamps": True,
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    if not row["ok"]:
        print(row["videoId"], "skipped:", row["reason"])
        continue
    print(row["title"], row["language"], "auto" if row["isAutoGenerated"] else "manual")
```

The same run is available over plain HTTP, and through the
[Apify MCP server](https://docs.apify.com/platform/integrations/mcp) if you are wiring it into an AI agent.

### Feeding a transcript to an LLM

Two things matter and both are in the output.

**Length.** A 20-minute talk is roughly 3,000 words. Send `text` when you want a summary; send `segments`
when the model has to cite a moment, because each segment carries `start` in seconds and you can turn that
straight into a `?t=` link back to the video.

**Provenance.** `isAutoGenerated` tells you whether the words were typed by a person or guessed by a
recogniser. Auto-generated tracks have no punctuation on many channels and will mis-hear names — which is
survivable for search and summarisation, and not survivable for quotation. Keep the flag on the row so the
model, or the person reading its answer, can tell the difference.

### Pricing

**You are charged only when a transcript with actual text comes back.** Private videos, videos without captions, and blocked requests cost you nothing.

That matters more than the headline number: a cheaper Actor that charges for empty rows costs more per *usable* transcript. On the 180-video run above, 17 videos produced nothing — a scraper that bills per input row would have charged for all 17.

### Typical uses

- Feeding video content to an LLM for summaries, Q\&A or RAG
- Searching across a channel's back catalogue
- Subtitling, translation and repurposing workflows
- Research and content analysis at scale

### FAQ

**Does it work on auto-generated captions?**
Yes. Manual captions are preferred when both exist, and `isAutoGenerated` tells you which you got. On the
2026-08-29 run, 120 of 163 transcripts were auto-generated.

**What about videos in other languages?**
Pass `languages` in your order of preference. `availableLanguages` lists everything the video has. If you ask
for English and the video has no English track, you get what it does have, and `language` says so.

**Why does it need a proxy?**
YouTube rate-limits repeated caption requests from one address. Requests are retried from a fresh IP automatically. Residential proxying is used only when it is actually needed.

**Will I be charged for a video with no captions?**
No. Charging happens after a transcript with text is produced.

**Can it take a whole channel?**
This Actor takes a list of videos. For a channel, use
[YouTube Channel Transcript Scraper](https://apify.com/neverempty/youtube-channel-transcripts), which resolves
the handle to a channel id first — handles get reused and redirected, so resolving them is not optional.

**How many videos can I send at once?**
There is no fixed cap. The run is sequential with retries, so a list of a few hundred is routine; for tens of
thousands, split the list across runs so a single block does not stall everything behind it.

### Other tools by NeverEmpty

Every NeverEmpty Actor follows the same rule: it never returns an empty result to mean two different things, and it only charges for rows that actually carry an answer.

- **[youtube-channel-transcripts](https://apify.com/neverempty/youtube-channel-transcripts)** - the same engine, pointed at a channel handle
- **[github-repo-search](https://apify.com/neverempty/github-repo-search)** - GitHub repository search through the official API, with activity columns
- **[seo-site-audit](https://apify.com/neverempty/seo-site-audit)** - technical SEO audit: meta, canonical, robots, headings

### Support

If a video that should work does not, open an issue on the **Issues** tab with the video ID. The `reason` field on the failed row tells me most of what I need.

# Actor input Schema

## `videos` (type: `array`):

YouTube video URLs (watch, youtu.be, shorts, embed) or bare 11-character video IDs.

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

Language codes in order of preference, e.g. en, ja, es. Manual captions are preferred over auto-generated ones.

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

Include the per-line segments with start time and duration in addition to the full text.

## `proxyMode` (type: `string`):

auto = try datacenter first (cheaper), fall back to residential when YouTube blocks. Use residential if you scrape at volume.

## `maxRetries` (type: `integer`):

How many times to retry a video with a fresh IP when YouTube blocks or rate-limits the request.

## Actor input object example

```json
{
  "videos": [
    "https://www.youtube.com/watch?v=aircAruvnKk"
  ],
  "languages": [
    "en"
  ],
  "includeTimestamps": true,
  "proxyMode": "auto",
  "maxRetries": 3
}
```

# Actor output Schema

## `results` (type: `string`):

All rows produced by 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 = {
    "videos": [
        "https://www.youtube.com/watch?v=aircAruvnKk"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/youtube-transcript-reliable").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 = { "videos": ["https://www.youtube.com/watch?v=aircAruvnKk"] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/youtube-transcript-reliable").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 '{
  "videos": [
    "https://www.youtube.com/watch?v=aircAruvnKk"
  ]
}' |
apify call neverempty/youtube-transcript-reliable --silent --output-dataset

```

## MCP server setup

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

```

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/kPRNnssI5G54gXyR6/builds/E0GJca5aw2Q21CFW2/openapi.json
