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

Get the spoken transcript of any YouTube video by URL, by channel, or by keyword search. No API key, no login. Falls back to any available language instead of failing when there is no English one, prefers human-written captions, and says why when a video has none. Full text plus timed segments.

- **URL**: https://apify.com/reportable\_broth/youtube-transcript-scraper.md
- **Developed by:** [Quiet Harvest](https://apify.com/reportable_broth) (community)
- **Categories:** Videos, AI, Developer tools
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.99 / 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 Scraper

Get the spoken transcript of any YouTube video — by URL, by channel, or by searching a keyword. No API key, no login, no cookies.

**$2.99 per 1,000 transcripts. You are only charged for transcripts actually returned.**

***

### Why this one

Most transcript scrapers ask YouTube for English and give up when there isn't one. A Korean beauty video with a Korean transcript comes back empty, and you are left guessing whether the video had captions at all.

This Actor does two things differently.

**It falls back instead of failing.** If your preferred language is not available, you get whatever transcript the video does have, preferring a human-written one over an auto-generated one. Across 39 long-form videos in a mixed English/Korean sample, **39 returned a transcript**.

**It tells you why when there is nothing.** Every row carries `transcript_status`. An empty transcript is never ambiguous:

| `transcript_status` | What it means |
|---|---|
| `ok` | Transcript returned |
| `disabled_by_uploader` | The uploader switched captions off. Nobody can retrieve this one. |
| `no_transcript` | The video has no caption track at all |
| `video_unavailable` | Private, deleted, or age-restricted |
| `blocked` | YouTube rate-limited the request |
| `error` | Something else went wrong |

***

### Three ways in

#### 1. Specific videos

```json
{
  "videoUrls": [
    "https://www.youtube.com/watch?v=OrElyY7MFVs",
    "https://youtu.be/dQw4w9WgXcQ",
    "https://www.youtube.com/shorts/58mOSOMn72g"
  ]
}
```

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

#### 2. A whole channel

```json
{
  "channelUrls": ["@aliabdaal", "https://www.youtube.com/@veritasium"],
  "maxVideosPerSource": 25
}
```

#### 3. Everything on a topic

```json
{
  "keywords": ["korean skincare", "glass skin"],
  "maxVideosPerSource": 20
}
```

Searches YouTube and transcribes the results. Useful when you want the conversation on a subject rather than one creator.

***

### Worked examples

**Feed an LLM or RAG index — full text only, no timestamps**

```json
{
  "channelUrls": ["@lexfridman"],
  "maxVideosPerSource": 50,
  "includeSegments": false
}
```

Dropping segments makes the dataset far smaller when you only need the words.

**Build subtitles — keep the timings**

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

Each line comes back as `{"start": 0.24, "duration": 3.84, "text": "..."}`.

**Non-English content**

```json
{
  "keywords": ["메이크업"],
  "languages": ["ko", "en"],
  "maxVideosPerSource": 30
}
```

**Long-form only, highest hit rate**

```json
{
  "keywords": ["web scraping tutorial"],
  "skipShorts": true,
  "maxVideosPerSource": 30
}
```

Most Shorts have captions switched off by their uploader. Skipping them raises the share of videos that come back with text.

**Research a competitor's whole channel**

```json
{
  "channelUrls": ["@competitor"],
  "maxVideosPerSource": 200,
  "includeSegments": false
}
```

**Python**

```python
import os
from apify_client import ApifyClient

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

run = client.actor("reportable_broth/youtube-transcript-scraper").call(run_input={
    "keywords": ["korean skincare"],
    "languages": ["ko", "en"],
    "maxVideosPerSource": 20,
})

for r in client.dataset(run.default_dataset_id).iterate_items():
    if r["transcript_status"] == "ok":
        print(f'{r["transcript_language"]:>6}  {len(r["transcript"]):>6} chars  {r["title"]}')
    else:
        print(f'  skipped ({r["transcript_status"]}): {r["title"]}')
```

> On `apify-client` 3.x the object returned by `.call()` is a model, not a dict. Use `run.default_dataset_id`, not `run["defaultDatasetId"]`.

***

### Output

| Field | Type | Notes |
|---|---|---|
| `video_id` | string | |
| `url` | string | Watch link |
| `title` | string | |
| `channel` | string or null | Channel name |
| `channel_id` | string or null | |
| `channel_username` | string or null | The @handle |
| `channel_thumbnail` | string or null | Channel avatar |
| `subscriber_count` | integer or null | Read from the video owner block; YouTube omits it on some page variants |
| `subscriber_count_text` | string or null | As YouTube shows it, e.g. `6.69M subscribers` |
| `thumbnail` | string or null | Video thumbnail |
| `published_at` | string or null | **Exact** ISO 8601 publish time |
| `published_ts` | integer or null | The same, as a Unix timestamp |
| `duration_seconds` | integer or null | |
| `view_count` | integer or null | Exact |
| `like_count` | integer or null | Exact |
| `comment_count_text` | string or null | As YouTube shows it, e.g. `4.2K` |
| `comment_count_approx` | integer or null | That text as a number — rounded |
| `comment_count` | integer or null | **Exact**. Only with `exactCommentCount` |
| `description` | string or null | Full description |
| `hashtags` | array | Parsed from the description |
| `description_links` | array | Parsed from the description |
| `keywords` | array | The uploader's tags |
| `is_short` | boolean or null | |
| `is_live` | boolean or null | |
| `available_languages` | array | Every caption track the video has: `code`, `name`, `is_generated` |
| `transcript_status` | string | See the table above |
| `transcript` | string or null | Full text |
| `transcript_chars` | integer | Length, handy for filtering |
| `transcript_segments` | array | `start`, `duration` and `text` per line |
| `transcript_language` | string or null | The language actually returned |
| `transcript_is_generated` | boolean | Auto-generated, or human-written |
| `_source` | string | `url`, `channel` or `keyword` |
| `_query` | string | What produced this row |

#### Sample row

```json
{
  "video_id": "OrElyY7MFVs",
  "url": "https://www.youtube.com/watch?v=OrElyY7MFVs",
  "title": "My Evidence-Based Skincare Routine",
  "channel": "Ali Abdaal",
  "transcript_status": "ok",
  "transcript": "All right, so having the perfect skincare routine is actually pretty simple...",
  "transcript_chars": 18705,
  "transcript_language": "en",
  "transcript_is_generated": true,
  "transcript_segments": [
    { "start": 0.0, "duration": 0.88, "text": "All right, so having the perfect" }
  ],
  "_source": "url",
  "_query": "OrElyY7MFVs"
}
```

***

### What to expect

**Not every video has a transcript, and that is not a bug.** Uploaders can switch captions off, and most Shorts have them off. In a 131-video sample across three keywords, 111 came back with a transcript; every one of the other 20 was `disabled_by_uploader`. With `skipShorts` on, a 39-video long-form sample returned 39.

**You are only charged for transcripts actually returned.** Rows with a non-`ok` status cost nothing.

**Residential proxy is the default and worth keeping.** YouTube rate-limits repeated requests from one IP, and a blocked request is retried on a fresh one.

**Public content only.** Private, unlisted and members-only videos are not accessible.

***

### FAQ

**Do I need a YouTube Data API key?**
No. No Google Cloud project, no quota, no login.

**What if a video has no English transcript?**
You get whatever transcript it has, and `transcript_language` tells you which. A human-written track is preferred over an auto-generated one when both exist.

**Can I get subtitles in SRT or VTT?**
The timed segments contain everything an SRT or VTT needs — start, duration and text — so converting is a few lines in your own code.

**Does it translate?**
No. You get the transcript in the language it exists in.

**Why is a transcript empty?**
Read `transcript_status`. `disabled_by_uploader` means the uploader turned captions off and no tool can retrieve it.

***

### Our other Actors

- **[Threads Scraper](https://apify.com/reportable_broth/threads-scraper-monitor)** — search Threads by keyword or pull a profile's posts, replies, reposts and media. No login.
- **[YouTube Monitor](https://apify.com/reportable_broth/youtube-scraper-monitor)** — watch keywords and channels for **new** videos only, with Shorts, exact publish times, like counts and built-in dedupe across runs.

***

### Changelog

- **0.1.0** — First release. Videos, channels and keyword search; language fallback; `transcript_status`.

### Disclaimer

This Actor collects publicly available captions only. You are responsible for how you use what you collect, including compliance with YouTube's terms of service and applicable law.

***

**Keywords:** youtube transcript, youtube transcript scraper, youtube captions, youtube subtitles, transcript api, video transcript, youtube to text, extract youtube transcript, bulk transcripts, transcript extractor, youtube captions api, srt, vtt, whisper alternative, no api key.

# Actor input Schema

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

YouTube video links. Watch links, Shorts links, youtu.be links and bare 11-character video IDs all work.

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

Transcribe a channel's latest videos. Handles work with or without the @, and full channel URLs are accepted.

## `keywords` (type: `array`):

Search YouTube and transcribe what comes back. Useful when you want the conversation on a topic rather than a specific channel.

## `maxVideosPerSource` (type: `integer`):

Applies to channels and keywords. Video URLs you list explicitly are always all transcribed.

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

Tried in order. A video with none of them still returns whatever transcript it has, preferring a human-written one over an auto-generated one — so you get a result instead of an error.

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

Keep the line-by-line transcript with start times and durations. Turn off for full text only, which makes the dataset much smaller.

## `subtitleFormats` (type: `array`):

Also return the transcript as a ready-to-use subtitle file, so you do not have to convert the timings yourself.

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

Translate the transcript into another language. Timings are kept, so you also get translated SRT/VTT if you asked for subtitle files. Leave empty to skip.

## `skipShorts` (type: `boolean`):

Most Shorts have captions switched off by their uploader. Skipping them raises the share of videos that come back with a transcript.

## `includeVideoDetails` (type: `boolean`):

On by default. Adds the video's exact publish time, like count, view count, full description, duration, channel ID, subscriber count and the list of languages its captions are available in. Costs one extra request per video.

## `exactCommentCount` (type: `boolean`):

Off by default. The video page only shows a rounded count like `4.2K`; turning this on fetches the exact number (4,296) at the cost of one extra request per video.

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

Residential is the default and recommended: YouTube rate-limits repeated requests from one IP.

## `concurrency` (type: `integer`):

How many videos to fetch at once.

## Actor input object example

```json
{
  "videoUrls": [
    "https://www.youtube.com/watch?v=OrElyY7MFVs"
  ],
  "channelUrls": [],
  "keywords": [],
  "maxVideosPerSource": 10,
  "languages": [
    "en"
  ],
  "includeSegments": true,
  "subtitleFormats": [],
  "translateTo": "",
  "skipShorts": false,
  "includeVideoDetails": true,
  "exactCommentCount": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "concurrency": 4
}
```

# Actor output Schema

## `transcripts` (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=OrElyY7MFVs"
    ]
};

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

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

```

## MCP server setup

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