# YouTube Transcript Extractor (`dami_studio/youtube-transcript-bulk-scraper`) Actor

YouTube transcript extractor and scraper: transcripts in bulk from video links, a whole channel or a playlist. Each row has the full text, timed cues, language and auto-generated flag, plus title, channel, duration and views. You pay for transcripts, not videos listed. $0.40 per 1,000 transcripts.

- **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
- **Features:** Limited permissions, Multi-tenant, Server interface
- **Stats:** 441 total users, 262 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.38 / 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.
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 extractor for a list of videos, a whole channel or a playlist, in bulk**: get one
row per video with the whole transcript as plain text, the per-cue timings, the language, and the
video's own details next to it. Channels and playlists are expanded into their videos for you, and
that expansion costs nothing.

It reads the caption tracks YouTube already holds. There is no speech recognition here, so a video
that was uploaded without captions has nothing to return and comes back as an uncharged diagnostic
row instead. You pay for transcripts, not for the videos a channel turned out to have.

| | |
|---|---|
| **Input** | Video links, bare 11-character ids, a channel URL, `@handle` or `UC...` id, or a playlist link or id |
| **Output** | One row per transcript: full text, timed cues, language and auto-generated flag, title, description, channel, duration and view count. Upload date and SRT or VTT files when you ask |
| **Ceiling** | 500 entries in, up to 5,000 transcripts out |
| **Account needed** | None |
| **Price** | $0.40 per 1,000 transcripts on the Free plan, a little less on paid plans. The free plan's $5 a month covers about 12,500 |

| Using an AI agent? | |
|---|---|
| Actor | `dami_studio/youtube-transcript-bulk-scraper` |
| Set one field | `videoUrls`, labelled "Videos, channels or playlists" in the Console |
| Cap the bill | `maxTotalChargeUsd` on the run, for example `0.10`: it never charges past that |
| MCP, one link | `https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/youtube-transcript-bulk-scraper` |
| How often | Weekly for a channel, or when a new video comes out |

### 🔍 What YouTube Transcript Extractor does

Mix whatever you have in one list. Watch links, Shorts links, `youtu.be` links, embed links, bare
video ids, a channel URL, an `@handle`, a playlist link. Channels are read newest first, playlists
in their own order, and a video that shows up twice across two sources is fetched once.

You get the transcript two ways on the same row. `text` is the whole thing as one readable string,
already joined up, which is what you want if it is going into a model or a search index. `segments`
keeps every cue with its `start`, `duration` and `end`, which is what you want for jumping to a
moment in the video or building captions.

Language is a preference list, not a filter. Put `en, es, pt-BR` in and the first one the video
actually has is used; `pt` matches `pt-BR`. A human-written track wins over the machine one unless
you say otherwise, because it has punctuation and speaker turns. Whichever track was used is named
on the row, and `availableLanguages` lists everything the video offered.

### 📋 What data you get from each YouTube transcript

| What you get | Field |
|---|---|
| The transcript as one string, with its word and character counts | `text`, `wordCount`, `characterCount` |
| Every cue with start, duration and end in seconds, when segments are on | `segments`, `segmentCount` |
| The track used: language, its name, machine-made or not | `language`, `languageName`, `isAutoGenerated` |
| Every track the video offered | `availableLanguages` |
| The video: id, link, title, length, views and thumbnail | `videoId`, `url`, `title`, `durationSeconds`, `viewCount`, `thumbnailUrl` |
| Its description, and whether it was a live stream | `description`, `wasLive` |
| When it went up and its category, when you ask | `publishedAt`, `category` |
| Ready subtitle files, when you ask | `srt`, `vtt` |
| The channel: name, id and link | `channelName`, `channelId`, `channelUrl` |
| Which of your entries produced the row | `sourceType`, `sourceUrl`, `playlistId` |

### ▶️ How to scrape YouTube transcripts in bulk

1. Open [YouTube Transcript Extractor](https://apify.com/dami_studio/youtube-transcript-bulk-scraper)
   and click **Try for free**.
2. Paste links into **Videos, channels or playlists**, one per line.
3. Set **Maximum transcripts** low for the first run, since that is what you pay for.
4. Put your language codes into **Preferred languages** in the order you want them tried.
5. Click **Start**, then download the dataset as JSON, CSV or Excel, or pull it from the Apify API.

Run it with the input empty and you get one labelled sample row, uncharged, so you can look at the
shape before you queue a real list.

### 💰 How much does it cost to scrape YouTube transcripts in bulk?

**$0.40 per 1,000 transcripts** on the Free plan. Paid plans pay a little less: $0.395 on Starter, $0.39 on Scale and $0.385 on Business. On the free plan, the
$5 Apify gives you each month covers about 12,500 transcripts.

Only a delivered transcript counts. Expanding a channel or a playlist into its videos adds nothing,
however many it finds. A video with no captions, a rejected entry, a video that would not open and
the sample row are all uncharged rows. A video your caption type or date window leaves out is not
charged either, and neither is the upload-date lookup.

A video that appears in two of your sources is fetched once and counted once.

### 📥 What you give it

```json
{
  "videoUrls": [
    "https://www.youtube.com/watch?v=aircAruvnKk",
    "https://www.youtube.com/@3blue1brown"
  ],
  "maxItems": 10,
  "languages": ["en"]
}
```

| Field | Default | What it is |
|---|---|---|
| `videoUrls` | none | Up to 500 entries, in any of the forms above, mixed freely. Bare channel ids (`UC...`) and playlist ids (`PL...`) work too. |
| `maxItems` | none (the form starts at `10`) | Hard ceiling on transcripts returned by the whole run. This is what you pay for, so keep it small while testing. A video your date window or caption type leaves out does not use up one of them, until the run has looked through four times this number of left-out videos (at least 100, at most 2,000). After that they count like any other video. |
| `maxVideosPerSource` | none (the form starts at `25`) | How deep to go into each channel or playlist before moving on. Videos your date window or caption type leaves out do not count toward it. Ignored for links that already point at one video. |
| `languages` | none (the form starts at `en`) | Language codes in priority order. Leave it empty to take whatever track the video ships with. |
| `preferAutoGenerated` | `false` | Turn it on when you specifically want the machine transcript even though a written one exists. |
| `captionType` | `any` | `human` keeps only human-written tracks, `auto` only auto-generated ones. A video without that kind gets a free row and is not charged. |
| `publishedAfter` | none | Only videos that went up on or after this day, as `2026-09-01` (UTC) or `30 days`. A channel stops at its first older video. Skipped videos are not charged. A channel or playlist stops after 200 videos outside the window, and its free `DATE_WINDOW_LIMIT` row says so. Adds the upload date to each row. |
| `publishedBefore` | none | Only videos that went up before this day, in the same formats, with the same 200-video stop. |
| `includeSegments` | `true` | Keeps the per-cue array on each row. Turn it off for plain text and much smaller rows. |
| `subtitleFormat` | `none` | `srt`, `vtt` or `both`: the transcript as subtitle-file text on the row. Same price. |
| `includePublishDate` | `false` | Adds `publishedAt` and `category`. One more lookup per video, so a little slower, same price. |
| `concurrency` | none (the form starts at `6`) | How many videos are read at once, 1 to 10. |
| `proxyUrls` | none | Optional. Your own servers, as `http://user:pass@host:port`. Leave it empty otherwise. |

### 📤 What you get back

![Real run output](https://api.apify.com/v2/key-value-stores/BTBHU3DdiuoF4JOsK/records/youtube-transcript-bulk-scraper-output.png)

A real row from a real run. `text` and `segments` are cut short here; on the real row `text` runs to
822 characters and `segments` has all 13 cues:

```json
{
  "ok": true,
  "charged": true,
  "recordType": "transcript",
  "videoId": "6XPlmCDNLNc",
  "url": "https://www.youtube.com/watch?v=6XPlmCDNLNc",
  "title": "The 64 sugar cubes puzzle",
  "channelName": "3Blue1Brown",
  "channelId": "UCYO_jab_esuFRV4b17AJtAw",
  "channelUrl": "https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw",
  "durationSeconds": 55,
  "viewCount": 322213,
  "thumbnailUrl": "https://i.ytimg.com/vi/6XPlmCDNLNc/sddefault.jpg?sqp=...",
  "language": "en",
  "languageName": "English",
  "isAutoGenerated": false,
  "segmentCount": 13,
  "wordCount": 160,
  "characterCount": 822,
  "text": "It's time for a new puzzle of the month. This one comes from a problem on the 2024 British Math Olympiad. ...",
  "segments": [
    { "start": 0, "duration": 1.7, "end": 1.7, "text": "It's time for a new puzzle of the month." },
    { "start": 2.08, "duration": 3.56, "end": 5.64, "text": "This one comes from a problem on the 2024 British Math Olympiad." }
  ],
  "availableLanguages": [
    { "languageCode": "en", "languageName": "English", "isAutoGenerated": false },
    { "languageCode": "en", "languageName": "English (auto-generated)", "isAutoGenerated": true }
  ],
  "sourceType": "channel",
  "sourceUrl": "https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw",
  "playlistId": "UUYO_jab_esuFRV4b17AJtAw",
  "scrapedAt": "2026-09-21T01:27:33.286Z"
}
```

| Field | How to read it |
|---|---|
| `text` | `wordCount` and `characterCount` measure this field. |
| `segments` | Present only while `includeSegments` is on. |
| `isAutoGenerated` | Whether the track actually used was the machine one. Worth keeping, since punctuation and accuracy differ a lot. |
| `sourceType`, `sourceUrl` | `video`, `channel` or `playlist`, and which of your entries it was. Useful when you pasted a mixed list. |
| `viewCount`, `durationSeconds` | Read off the video itself at the time of the run. |
| `description` | As YouTube shows it under the video, up to 5,000 characters. |
| `wasLive` | `true` for a live stream or its replay. |
| `publishedAt` | UTC time the video went up. Only there when you asked for the date or set a date window. |
| `srt`, `vtt` | Only there when you asked. Save the text as a `.srt` or `.vtt` file as it is. |

### 🧾 Reading the output

Three kinds of row land in your dataset, and `recordType` separates them.

| Row | How to spot it | Billed |
|---|---|---|
| A real transcript | `recordType: "transcript"`, `ok: true` | yes |
| The sample row | `recordType: "sample"`, `_sample: true` | no |
| A diagnostic | `recordType: "diagnostic"`, `ok: false`, an `errorCode` | no |

Filter on `recordType == "transcript"` and you have your transcripts. The `charged` flag is written
onto the row as it is built, so treat it as the marker that tells a real row from a sample or a
diagnostic, not as a receipt for what was billed.

| Code | What it means |
|---|---|
| `BAD_INPUT` | That entry was not a video, channel or playlist link. Past 25 of them, one row counts the rest. |
| `NOT_FOUND` | The handle, channel or video does not exist any more. |
| `NO_RESULTS` | Nothing to return: a video with no captions or none of the caption type you asked for, a channel with no uploads, a private playlist, a source with nothing in your date window. |
| `AGE_RESTRICTED` | That video is age-restricted, so its captions can't be read. |
| `DATE_UNKNOWN` | The upload date could not be read this run, so the video was left out of your date window and not charged. Re-run it. |
| `DATE_WINDOW_LIMIT` | That channel or playlist stopped after 200 videos outside your date window. Narrow the window or lower **Videos per channel or playlist**. |
| `FILTER_LIMIT` | That channel or playlist was cut short because the run had already looked through as many left-out videos as it gives places back for: four times **Maximum transcripts**, at least 100 and at most 2,000. After that, left-out videos count toward **Maximum transcripts** again. Narrow the window or the caption type, or split the list over more runs. |
| `NOT_REACHED` | The run ended before it got to that entry, so it was not read. Usually **Maximum transcripts** was already used up. Run that entry again on its own. |
|`RATE_LIMITED`, `SERVER_ERROR` | That video would not answer this run. Re-running usually clears it. |
| `NOT_SERVED` | This transcript was not served this run. Re-run it later. Runs before 13 October 2026 wrote `BLOCKED` here. |
| `NETWORK` | The connection failed, or the video would not open for a reason it did not name. |
| `TIMEOUT` | YouTube did not answer in time for that video, channel or playlist. Re-running usually clears it. |
| `TIME_BUDGET` | The run ran out of time before reaching that video. One row per video left over. The run still ends as succeeded, with an amber status message, even when the time limit stopped it before any transcript came back. |
| `PROXY_INPUT_ADJUSTED` | A setting you gave in `proxyUrls` could not be used and was replaced. |
| `CHARGE_ERROR` | Billing failed on a row that was already delivered. Tell us if you see it. |
| `UNEXPECTED_ERROR` | Something broke that the run did not classify. Send the run ID. |

### 💡 What people use it for

- Making a video searchable. Keep `segments` and a hit gives you the second to jump to.
- Keeping up with a channel. Set **Published on or after** to `7 days` and a weekly run picks up only what is new.

From a channel to the reaction to what it said, in two runs:

1. Paste a channel here with **Videos per channel or playlist** at 50 and keep the `text` column.
2. Filter the rows on a phrase in `text` to find the videos that mention it.
3. Paste their `url` into the **Video URLs** box of
   [YouTube Comments Scraper](https://apify.com/dami_studio/youtube-comments-scraper) to read how
   viewers took it.

### 🚧 What it does not do

- **Captions only.** There is no speech recognition. If the uploader published no caption track, no
  amount of retrying will produce text.
- **No translation.** You get the tracks the video already carries. Asking for `fr` on a video with
  only English gets you the English track, not a translation.
- **Channels come from the uploads list, newest first.** That misses unlisted videos and sometimes
  misses Shorts. A date window stops a source after 200 videos outside it.
- **A watch link carrying `list=` is treated as one video**, not as the playlist. Paste the playlist
  link itself if you want the playlist.
- **The 5,000 ceiling is the setting's maximum, not a measured promise.** Long videos make big rows:
  a 3 hour 13 minute podcast came back as a single 160 KB row.
- **Private videos are reported as a failure to open**, not as a reason of their own. Age-restricted
  videos get their own `AGE_RESTRICTED` row.
- It is not an official YouTube tool. Dami's Studio is independent and is not affiliated with or endorsed by YouTube or Google, or by any other company named on this page.

### 🧭 Which YouTube scraper do you need?

| If you want | Use |
|---|---|
| Transcripts for a whole channel or playlist in one run | This one |
| What a single video says, as text, SRT and VTT | [YouTube Transcript Scraper](https://apify.com/dami_studio/youtube-transcript-scraper) |
| A channel's uploads as rows, up to 2,000 per run | [YouTube Channel Scraper](https://apify.com/dami_studio/youtube-channel-scraper) |
| Videos in a playlist | [YouTube Playlist Scraper](https://apify.com/dami_studio/youtube-playlist-scraper) |
| Shorts from a channel or a keyword, with exact view and like counts | [YouTube Shorts Scraper](https://apify.com/dami_studio/youtube-shorts-scraper) |
| Only the new uploads since your last check | [YouTube Upload Monitor](https://apify.com/dami_studio/youtube-channel-monitor) |
| The video, its audio or its subtitles as a file | [YouTube Downloader Pro](https://apify.com/dami_studio/youtube-video-downloader) |

### ❓ Questions people ask

#### Do I need a YouTube API key?

No. No key, no OAuth, no quota to manage.

#### What if the video has no captions?

You get a `NO_RESULTS` row naming that video and the run moves on. That row is not charged.

#### Auto-generated or written captions, which do I get?

The written one when the video has it, because it is punctuated. Tick **Prefer auto-generated
captions** to flip that. To get only one kind and skip videos without it, set **Caption type**.

#### Can I get the timings?

Yes, that is `segments`, and it is on by default. Turn it off when you want smaller rows and only
the plain text.

#### Can I call it from code or connect it to an AI assistant?

The [API tab](https://apify.com/dami_studio/youtube-transcript-bulk-scraper/api/python) has code for Python,
JavaScript and the CLI. MCP clients like Claude and ChatGPT use
`https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/youtube-transcript-bulk-scraper`; coding assistants can
load [our skill file](https://api.apify.com/v2/key-value-stores/BTBHU3DdiuoF4JOsK/records/apify-youtube-SKILL.md). All
three bill your Apify account at the same price.

```python
## pip install apify-client
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("dami_studio/youtube-transcript-bulk-scraper").call(
    run_input={
        "videoUrls": ["https://www.youtube.com/watch?v=aircAruvnKk"],
        "maxItems": 10,
    },
    max_total_charge_usd=0.10,  # the most this run may spend
)
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    if row.get("ok") is False:  # free note rows, not results
        continue
    print((row.get("title") or "")[:80], row.get("wordCount"))
```

#### Is scraping YouTube transcripts legal?

This reads publicly available caption tracks. Transcripts are somebody's work, so what you may do
with them afterwards is a copyright question rather than a scraping one. Apify's write-up on
[the legality of web scraping](https://blog.apify.com/is-web-scraping-legal/) is a reasonable
starting point, and we are not lawyers.

### 🆘 If something breaks

Open the **Issues** tab on the actor page. Include the links you used and the run ID. The diagnostic
rows in your dataset usually name the reason already.

# 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, a bare channel ID (UC...), a playlist link or a bare playlist ID (PL...). 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. A video your date window or caption type leaves out does not use up one of them.

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

How deep to go into each channel or playlist before moving to the next one. Channels are read newest first. Videos your date window or caption type leaves out do not count toward it. 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.

## `captionType` (type: `string`):

Any takes the best track the video has, as before. Human-written only skips a video that has nothing but auto captions; auto-generated only does the reverse. A skipped video gets a free row saying why and is not charged.

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

Only videos that went up on or after this day: a date like 2026-09-01 (UTC), or a number of days back like 30 days. A channel is read newest first, so the run stops at its first older video. Videos outside the window are skipped before their captions are read and are not charged. A channel or playlist stops after 200 videos outside the window, and its free row says so. Setting a date also adds the upload date to each row.

## `publishedBefore` (type: `string`):

Only videos that went up before this day, in the same formats. Skipped videos are not charged, and a channel or playlist stops after 200 videos outside the window, with a free row saying so.

## `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.

## `subtitleFormat` (type: `string`):

Adds the transcript to each row as a ready subtitle file: srt, vtt or both, as text you can save straight to a .srt or .vtt file. Same price per transcript.

## `includePublishDate` (type: `boolean`):

Adds publishedAt (when the video went up) and category to each row. It takes one more lookup per video, so the run is a little slower; the price is the same.

## `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 for a normal run. Fill it in only if you want the traffic to leave through proxy servers you already pay for, one URL per line, 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,
  "captionType": "any",
  "includeSegments": true,
  "subtitleFormat": "none",
  "includePublishDate": false,
  "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 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/FVLPUqg0q0E6TeQZn/openapi.json
