# YouTube Transcript Bulk Scraper - $0.40 per 1,000 (`dami_studio/youtube-transcript-bulk-scraper`) Actor

Bulk YouTube transcripts from video links, a channel URL or a playlist: full text, per-cue timestamps, language, auto-generated flag, plus title, channel, duration and view count. Channel and playlist expansion is free and videos without captions are never charged. No API key.

- **URL**: https://apify.com/dami\_studio/youtube-transcript-bulk-scraper.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** Videos, AI, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.40 / 1,000 transcript extracteds

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?

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

Paste video links, a channel URL or @handle, or a playlist link. Every video that has captions comes
back as one row: the full transcript text, the per-cue timestamps, the language, whether the track
was written by a human or generated by machine, plus the video title, channel, duration and view
count.

Channels and playlists get expanded into their videos for you, and that expansion is free. You pay
only for transcripts that actually come back. A 900-video channel with `maxItems: 20` costs you 20
transcripts, not 900 page loads.

It reads captions from YouTube's own player API as a first-time guest. No API key, no Google
account, no browser.

### Price

$0.40 per 1,000 transcripts, plus a $0.0005 start fee per run.

| What | Cost |
|---|---|
| One transcript | $0.0004 |
| 1,000 transcripts | $0.40 |
| Starting a run | $0.0005 |
| A run that returns nothing | $0.0005 |

One `transcript-scraped` event per transcript row. Free: listing a channel or playlist, resolving an
@handle, the sample row, and every diagnostic row — a video with no captions, a deleted video, a
private video, a time-budget skip. A video that appears twice in your input, or in two of your
playlists, is fetched and charged once.

### Input

```json
{
  "videoUrls": [
    "https://www.youtube.com/watch?v=aircAruvnKk",
    "https://www.youtube.com/@3blue1brown",
    "https://www.youtube.com/playlist?list=PLZHQObOWTQDNU6R1_67000Dx_ZCJB-3pi"
  ],
  "languages": ["en"],
  "maxItems": 50,
  "maxVideosPerSource": 25
}
```

| Field | What it does |
|---|---|
| `videoUrls` | Mix and match: watch / Shorts / youtu.be / embed links, bare 11-character video IDs, a channel URL or @handle, or a playlist link. Up to 500 entries. |
| `maxItems` | Total transcripts this run returns across everything you pasted. This is what you pay for. Default 10, ceiling 5,000. |
| `maxVideosPerSource` | How deep to go into each channel or playlist before moving on. Channels are read newest first. Default 25, ceiling 2,000. |
| `languages` | Codes in priority order, e.g. `["en","es","pt-BR"]`. The first one the video actually has wins, and `pt` matches `pt-BR`. **Set this.** See the note below. |
| `preferAutoGenerated` | Off by default, so a human-written track beats a machine one — it has punctuation and speaker turns. Turn it on if you specifically want the machine transcript. |
| `includeSegments` | On by default. Keeps the per-cue array on each row. Turn it off for smaller rows when you only want the text. |
| `concurrency` | Videos fetched at once, 1–10. Default 6. |
| `proxyUrls` | Leave empty unless you already pay for proxy servers and want traffic to leave through them. |

**About `languages`.** If you leave it empty you get whatever caption track YouTube lists first,
preferring human-written over machine. On a channel with community translations that is often not the
spoken language — a run against a 3Blue1Brown video with no `languages` set came back with the
Arabic track, because it exists and is human-written. Put `["en"]` in there if you want English.

Empty input gives you one labelled sample row, free.

### Output

```json
{
  "ok": true,
  "charged": true,
  "recordType": "transcript",
  "videoId": "aircAruvnKk",
  "url": "https://www.youtube.com/watch?v=aircAruvnKk",
  "title": "But what is a neural network? | Deep learning chapter 1",
  "channelName": "3Blue1Brown",
  "channelId": "UCYO_jab_esuFRV4b17AJtAw",
  "channelUrl": "https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw",
  "durationSeconds": 1120,
  "viewCount": 23954297,
  "thumbnailUrl": "https://i.ytimg.com/vi/aircAruvnKk/sddefault.jpg",
  "language": "en",
  "languageName": "English",
  "isAutoGenerated": false,
  "segmentCount": 217,
  "wordCount": 2380,
  "characterCount": 13433,
  "text": "This is a 3. It's sloppily written and rendered at an extremely low resolution...",
  "segments": [
    { "start": 4.22, "duration": 1.18, "end": 5.4, "text": "This is a 3." }
  ],
  "availableLanguages": [
    { "languageCode": "en", "languageName": "English", "isAutoGenerated": false },
    { "languageCode": "es", "languageName": "Spanish", "isAutoGenerated": false }
  ],
  "sourceType": "video",
  "sourceUrl": "https://www.youtube.com/watch?v=aircAruvnKk",
  "playlistId": null,
  "scrapedAt": "2026-08-21T16:20:26.000Z"
}
```

- `availableLanguages` lists every track the video has, so you can see what you could have asked for.
- `sourceType` / `sourceUrl` say where the video came from — the link you pasted, or the channel or
  playlist it was expanded out of. Useful when one run mixes several sources.
- `playlistId` for a channel is its uploads playlist. Stable and re-usable.
- `wordCount` / `characterCount` are counted on `text`, which is what you want for budgeting a
  language-model pass.

### Limits

- **It reads captions. It does not transcribe audio.** A video with no caption track at all produces
  an uncharged `NO_RESULTS` row. Music videos, ambient footage and brand-new uploads are the usual
  cases — in a sample of eighty recent uploads across ten large channels, six had no track.
- Auto-generated tracks have no punctuation, no capitalisation and no speaker labels, and they mangle
  names and technical terms. `isAutoGenerated` tells you which rows are affected.
- Private, deleted, members-only, age-restricted and region-blocked videos come back as uncharged
  diagnostics.
- Live streams have nothing usable until the recording is processed and captions appear.
- Channels are listed newest first from their uploads playlist. There's no date filter and no
  oldest-first order — ask for more videos per source and filter afterwards.
- A channel's uploads playlist leaves out unlisted videos and, on some channels, Shorts. Paste a
  Shorts link or the specific playlist if you need those.
- No translation. You get the tracks the video publishes. Ask for a language it doesn't have and you
  fall back to one it does; `language` on the row tells you which.
- Timings come from YouTube's caption file. Human tracks are cue-accurate; auto tracks drift by a
  fraction of a second and break lines mid-sentence.
- Very long videos make very large rows. A four-hour lecture is a few thousand cues — turn
  `includeSegments` off if you only need the text.
- Ceilings: 500 pasted entries, 2,000 videos per source, 5,000 transcripts per run.

### Diagnostic codes

Uncharged rows carry `"_diagnostic": true` and an `errorCode`: `NO_RESULTS` (no caption track, or the
caption file came back empty), `NOT_FOUND` (private, deleted, region-blocked, or the channel doesn't
exist), `BAD_INPUT` (not a YouTube link), `TIME_BUDGET`, `NETWORK`. Each carries the `videoId` and a
plain-English `details`.

Filter on `charged == true` and the count equals what you were billed for.

### How it gets through

Traffic leaves through a large rotating pool. YouTube shows some addresses a "sign in to confirm
you're not a bot" wall, and the run treats that as a signal to move rather than to wait: it hops to
another address, mints a fresh guest token there and carries on. That's what lets long channel runs
finish. Caption text is downloaded in YouTube's leanest caption format rather than the verbose one
the player defaults to — same cues, same timings, about a ninth of the bytes.

### Questions

**Do I need a YouTube API key?** No. Nothing is signed in and no quota is consumed.

**Can I give it a channel and get everything?** Paste the channel URL or @handle and raise
`maxVideosPerSource` and `maxItems`. Listing the channel is free.

**Will the run fail if YouTube blocks it?** No. It moves to another address and continues. If it
genuinely can't get through you get uncharged diagnostic rows and a succeeded run.

**Can I schedule it?** Yes. Point it at a channel daily and diff on `videoId` to pick up what's new.

# Actor input Schema

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

Mix and match: watch / Shorts / youtu.be / embed links, bare 11-character video IDs, a channel URL or @handle, or a playlist link. Channels and playlists are expanded into their videos for you, and that expansion is never charged.

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

Hard ceiling on how many transcripts this run returns in total, across everything you pasted. This is what you pay for, so keep it low while testing.

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

How deep to go into each channel or playlist before moving to the next one. Channels are read newest first. Ignored for links that already point at a single video.

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

Language codes in priority order, for example en, es, pt-BR. The first one the video actually has is used; pt matches pt-BR. Leave empty to take whatever caption track the video ships with.

## `preferAutoGenerated` (type: `boolean`):

Off by default, so a human-written caption track wins whenever the video has one - it has punctuation and speaker turns, which auto captions do not. Turn this on if you specifically want the machine transcript.

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

Keep the per-cue array of start / duration / end / text on each row. Turn it off if you only want the plain transcript text and smaller rows.

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

How many videos are fetched at the same time. Higher is faster and cheaper per row; lower is gentler if you are pulling several thousand transcripts in one run.

## `proxyUrls` (type: `array`):

Leave this empty. By default the run rotates a large pool of addresses that cost you nothing per gigabyte. Fill it in only if you specifically want the traffic to leave through proxy servers you already pay for, in the form http://user:pass@host:port.

## Actor input object example

```json
{
  "videoUrls": [
    "https://www.youtube.com/watch?v=aircAruvnKk",
    "https://www.youtube.com/@3blue1brown"
  ],
  "maxItems": 10,
  "maxVideosPerSource": 25,
  "languages": [
    "en"
  ],
  "preferAutoGenerated": false,
  "includeSegments": true,
  "concurrency": 6
}
```

# Actor output Schema

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

Every row in the default dataset: videoId, title, channelName, durationSeconds, language, languageName, isAutoGenerated, segmentCount, wordCount, text, url, sourceType, sourceUrl, channelUrl, channelId, viewCount, characterCount, thumbnailUrl, segments, availableLanguages, playlistId, scrapedAt. An empty, blocked or unmatched run returns a single uncharged row explaining what happened instead.

# 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",
        "https://www.youtube.com/@3blue1brown"
    ],
    "maxItems": 10,
    "maxVideosPerSource": 25,
    "languages": [
        "en"
    ],
    "concurrency": 6
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/youtube-transcript-bulk-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",
        "https://www.youtube.com/@3blue1brown",
    ],
    "maxItems": 10,
    "maxVideosPerSource": 25,
    "languages": ["en"],
    "concurrency": 6,
}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/youtube-transcript-bulk-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",
    "https://www.youtube.com/@3blue1brown"
  ],
  "maxItems": 10,
  "maxVideosPerSource": 25,
  "languages": [
    "en"
  ],
  "concurrency": 6
}' |
apify call dami_studio/youtube-transcript-bulk-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/youtube-transcript-bulk-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/hw1XdGtilIVp3p0BZ/builds/OkuRfCeJrRvcFutSC/openapi.json
