# YouTube Transcript Scraper — Subtitles & Captions, Timestamps (`nick.cheng/youtube-transcript-scraper`) Actor

Get YouTube transcripts from video links, IDs or channels: the captions in the spoken language (human-written when available, else auto-generated, de-duplicated) as clean text plus timed segments. $0.005 per transcript on every plan. No captions, no charge. No API key.

- **URL**: https://apify.com/nick.cheng/youtube-transcript-scraper.md
- **Developed by:** [Nicholas Cheng](https://apify.com/nick.cheng) (community)
- **Categories:** Videos, AI, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 transcript delivereds

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

**Give it YouTube links, video IDs or channels — get back what is said in each video, as clean text with timestamps.** The transcript is the caption track in the language that is actually spoken: the human-written captions when the video has them, otherwise YouTube's auto-generated ones, de-duplicated so the text reads as one pass of the speech. No API key, no login, no cookies.

**You pay only for transcripts that came back.** A video with no captions, a private or deleted video, or one YouTube would not serve produces an explanatory row and costs nothing.

| | |
|---|---|
| **Input** | Video URLs / IDs (watch, Shorts, youtu.be, embed) and/or channels (@handle, channel URL, UC… id) for their latest uploads |
| **Output** | One row per transcript: plain text + timed segments + title, channel, duration, language |
| **Price** | **$0.005 per transcript — the same on every Apify plan** |
| **Not charged** | Videos without captions, unavailable videos, filtered-out videos |

***

### What you get

Real output for `jNQXAC9IVRw` (the first video on YouTube):

```json
{
  "itemType": "transcript",
  "videoId": "jNQXAC9IVRw",
  "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
  "title": "Me at the zoo",
  "channelId": "UC4QobU6STFB0P71PMvOGN5A",
  "channelName": "jawed",
  "durationSeconds": 19,
  "transcriptLanguage": "en",
  "transcriptType": "manual",
  "transcript": "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 (baaaaaaaaaaahhh!!) and that's pretty much all there is to say",
  "subtitles": [
    {
      "language": "en",
      "type": "manual",
      "name": "English",
      "text": "All right, so here we are, in front of the elephants … and that's pretty much all there is to say",
      "segments": [
        { "start": 1.2, "end": 3.36, "text": "All right, so here we are, in front of the elephants" },
        { "start": 5.318, "end": 7.974, "text": "the cool thing about these guys is that they have really..." },
        { "start": 7.974, "end": 12.616, "text": "really really long trunks" },
        { "start": 12.616, "end": 14.367, "text": "and that's cool" },
        { "start": 14.421, "end": 15.733, "text": "(baaaaaaaaaaahhh!!)" },
        { "start": 16.881, "end": 18.881, "text": "and that's pretty much all there is to say" }
      ]
    }
  ],
  "availableCaptionLanguages": [
    { "language": "en", "isAutoGenerated": false },
    { "language": "de", "isAutoGenerated": false }
  ]
}
```

- `transcript` — the whole thing as one string, ready for an LLM, a search index or a summary.
- `segments` — the same text as timed cues (`start` / `end` in seconds), for quoting a moment or building chapters.
- `transcriptType` — `manual` (written by a person) or `auto-generated` (YouTube's speech recognition).
  Auto-generated captions arrive as "roll-up" cues that repeat each line two or three times; they are
  de-duplicated here, which is a common problem in raw auto-caption dumps.
- `availableCaptionLanguages` — every caption track the video has, so you know whether a translation exists.

***

### Input

```json
{
  "videoUrls": ["https://www.youtube.com/watch?v=aircAruvnKk", "jNQXAC9IVRw"],
  "channelUrls": ["@veritasium"],
  "maxVideosPerChannel": 5
}
```

| Option | Default | Notes |
|---|---|---|
| `videoUrls` | — | Watch, Shorts, `youtu.be`, embed, live links, or bare 11-character IDs |
| `channelUrls` | — | Transcribe a channel's latest uploads. `@handle`, channel URL, or `UC…` id |
| `maxVideosPerChannel` | `5` | Per channel, counting only videos that actually had a transcript |
| `minViews`, `minDurationSeconds`, `maxDurationSeconds`, `titleContains` | — | Skip videos you do not want before paying for them — e.g. `minDurationSeconds: 61` drops Shorts |
| `publishedAfter` | — | ISO date. Applies to channel uploads (read from the channel listing); links you pass directly are not date-filtered |
| `maxItems` | `0` | Hard cap on transcripts for the run — bounds your spend |

A channel page lists about 30 recent uploads; that is the pool `maxVideosPerChannel` draws from.

***

### Pricing

**$0.005 per transcript, on every Apify plan** — Free, Bronze, Silver, Gold, Platinum, Diamond. Check the
Pricing tab: all six columns are identical. There is no start fee and no per-minute fee.

The event is charged once per transcript **delivered**. Being straight about how that compares: several
transcript Actors charge per video processed or per dataset row, which includes videos that turned out to
have no captions. Here those rows are free, so the price you see is the price per transcript you actually get.

#### What you are not charged for

| | What happens here |
|---|---|
| **Videos without captions** | One row with `chargedForThis: false` and the reason. Common for music videos and clips without speech. |
| **Unavailable videos** | Private, deleted, region-blocked or age-restricted videos produce one explanatory row, not billed. |
| **Bot checks** | If YouTube keeps refusing after eight tries from different addresses and clients, the row says so and is not billed. |
| **Filtered-out videos** | Not written, not billed. |
| **The free plan** | Same code path, same fields, same price. No demo mode, no row cap. |

***

### What this Actor cannot do

- **No speech recognition of our own.** The transcript is YouTube's own captions. If YouTube has none for a
  video, you get none — we do not download the audio and transcribe it.
- **One language per video.** You get the spoken language. Translations YouTube offers are listed in
  `availableCaptionLanguages` but not downloaded.
- **No members-only or private videos**, and no age-restricted ones — those need a signed-in account and this
  Actor never signs in.
- **No search.** It works from links and channels, not keywords.
- For view counts, likes, publish dates and full descriptions, use the
  [YouTube Video Stats Scraper](https://apify.com/nick.cheng/youtube-video-stats) (it can include the
  transcript too, with `includeSubtitles`).

***

### How it stays working

The caption list is fetched the way YouTube's mobile apps fetch it — the iOS, Android VR and Android app
clients in turn — first from a datacenter address and, if YouTube shows a bot check, from fresh residential
addresses, up to eight tries. The caption file itself is downloaded directly. The watch page is never loaded, so a
transcript costs a fraction of a full video scrape.

Measured over three rounds of fifteen videos (news, lectures, music; English, German, Korean and Arabic
speech): the caption list came back for 14, 15 and 15 of 15 on a single pass through the clients, and all
170 caption downloads returned text. Client settings follow the open-source yt-dlp project and are updated
when YouTube changes them.

***

### Integrating

```bash
curl -X POST "https://api.apify.com/v2/acts/nick.cheng~youtube-transcript-scraper/run-sync-get-dataset-items?token=<TOKEN>" \
  -H 'Content-Type: application/json' \
  -d '{"videoUrls":["aircAruvnKk","jNQXAC9IVRw"]}'
```

Official clients exist for JavaScript and Python; results export as JSON, CSV or Excel; and the Actor is
available as a tool to AI agents through Apify's MCP server.

# Actor input Schema

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

YouTube videos to scrape. Accepts watch links, Shorts links, youtu.be links, embed links, or bare 11-character video IDs.

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

Channels whose recent videos should be scraped. Accepts @handles, /channel/UC… URLs, or plain UC… IDs. Each channel contributes up to 'Max videos per channel' videos.

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

How many recent videos to take from each channel. Applies per channel, so one run can cover many channels without one of them eating the whole budget.

## `maxItems` (type: `integer`):

Hard ceiling on rows for the whole run, across every video and channel. Leave at 0 for no limit.

## `minViews` (type: `integer`):

Skip videos with fewer views than this. Videos from channel listings are screened before their page is fetched, so filtered-out videos cost nothing. 0 means no minimum.

## `minDurationSeconds` (type: `integer`):

Skip videos shorter than this many seconds. Useful for dropping Shorts. 0 means no minimum.

## `maxDurationSeconds` (type: `integer`):

Skip videos longer than this many seconds. Useful for keeping only Shorts or clips. 0 means no maximum.

## `publishedAfter` (type: `string`):

ISO date or datetime, e.g. 2026-01-01. Videos published before it are skipped. Channel listings only show relative dates ("3 weeks ago"), so the pre-fetch screen is conservative and the exact date is checked once the page is loaded.

## `titleContains` (type: `array`):

Keep only videos whose title contains at least one of these words or phrases, case-insensitive. Leave empty to keep all titles.

## Actor input object example

```json
{
  "videoUrls": [
    "https://www.youtube.com/watch?v=aircAruvnKk",
    "jNQXAC9IVRw"
  ],
  "channelUrls": [
    "@veritasium"
  ],
  "maxVideosPerChannel": 5,
  "maxItems": 0,
  "minViews": 0,
  "minDurationSeconds": 0,
  "maxDurationSeconds": 0
}
```

# Actor output Schema

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

No description

## `summary` (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=aircAruvnKk",
        "jNQXAC9IVRw"
    ],
    "channelUrls": [
        "@veritasium"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("nick.cheng/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=aircAruvnKk",
        "jNQXAC9IVRw",
    ],
    "channelUrls": ["@veritasium"],
}

# Run the Actor and wait for it to finish
run = client.actor("nick.cheng/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=aircAruvnKk",
    "jNQXAC9IVRw"
  ],
  "channelUrls": [
    "@veritasium"
  ]
}' |
apify call nick.cheng/youtube-transcript-scraper --silent --output-dataset

```

## MCP server setup

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