# YouTube Transcript Scraper & API - Transcripts + Comments (`superxcoder/youtube-transcripts-scraper`) Actor

YouTube transcript API and scraper: get transcripts, captions and subtitles in the language you choose from videos, channels, playlists or search, plus top comments. Timestamped segments, text or markdown chunks for AI, RAG, n8n and MCP. No-caption videos are free. $0.80 per 1,000 transcripts.

- **URL**: https://apify.com/superxcoder/youtube-transcripts-scraper.md
- **Developed by:** [Deependra Singh](https://apify.com/superxcoder) (community)
- **Security:** Limited permissions
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 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 & API

**Get the transcript (captions and subtitles) of any public YouTube video, channel, playlist or search, in the language you choose, with views, likes and top comments, ready for Google Sheets, ChatGPT or your own code.**

$2 per 1,000 transcripts, $0.50 per 1,000 comments, no start fee. Videos without captions are free.

### Who is it for?

- **YouTube creators and marketers:** get the transcripts of a competitor's last 50 videos with views, likes and top comments, and find the topics and hooks that worked.
- **Students and researchers:** turn a lecture playlist into plain-text notes in English, Spanish, Hindi or any other language the video has captions in.
- **Knowledge bases for ChatGPT, Claude or NotebookLM:** get transcripts as Markdown sections with a timestamp and a link to the exact moment in the video.
- **SEO and content teams:** search YouTube for a keyword and read what the top videos say.
- **Developers and AI agents:** a YouTube transcript API for code, n8n, Make, Zapier and MCP (Claude, Cursor), without managing proxies or blocked IPs.

### How to use it in 3 steps

1. **Fill in the form.** Open the Actor in Apify Console (a free Apify account comes with monthly free credit, enough for thousands of transcripts). The **Input** tab shows a form. Paste a channel address into **Channels** (for example `https://www.youtube.com/@mkbhd`), video links into **Video links**, playlist links into **Playlists**, or words into **Search words**. The form starts with one example video; remove it if you only want your own channels, playlists or searches, or its transcript is added (and charged) too.
2. **Choose the details.** Set **Videos per channel** (for example 50), your **Transcript languages** (`en` for English, `es` for Spanish, `hi` for Hindi) and the **Transcript format**: **Plain text** for spreadsheets, **Markdown sections** for ChatGPT or Claude. Turn on **Include comments** if you want them.
3. **Click Start, then open the results.** The green **Start** button is at the bottom of the form. When the run finishes (1 to 2 minutes for 50 videos), the **Output** tab shows the results in tabs: **Overview** (views, likes, length, language), **Transcripts**, **Markdown** and **Comments**. Click **Export** to download them as CSV, Excel or JSON.

#### Recipe: a competitor's last 50 videos in Google Sheets

1. **Channels:** `https://www.youtube.com/@aliabdaal` (copy the address from your browser while you are on the channel page). Remove the example video.
2. **Videos per channel:** `50`. **Transcript format:** Plain text. **Include comments:** on. **Comments per video:** `5`.
3. Click **Start**. It costs about $0.20 (see "How much does it cost?").
4. In the **Output** tab, open the **Overview** or **Transcripts** tab, click **Export**, choose **CSV** and download the file.
5. In Google Sheets: **File > Import > Upload**, pick the file, **Insert new sheet**.
6. Sort by **Position** (1 = newest video) or by **Views**. Rows can arrive in any order, because videos are fetched in parallel. In the downloaded file the columns have their API names: `source.position` (Position), `viewCount` (Views), `likeCount` (Likes), `durationSeconds` (Length), `publishedAt` (Published), `transcriptText` (Transcript).

Good to know for spreadsheets:

- **Google Sheets holds at most 50,000 characters in one cell, and Excel at most 32,767.** Speech is about 1,300 characters per minute, so transcripts of videos longer than about 38 minutes (25 minutes for Excel) are cut off. For long videos, choose the **Markdown sections** format and export the **Markdown** tab (one short section per row), or download JSON.
- **Export a tab, not "All fields".** The full export turns every list into many columns (for example `availableLanguages/0/code`, `comments/0/text`), which gives 100+ columns. The tabs above have only the useful columns. In the Export dialog you can also pick the columns you want.
- **Dates:** `Published` is either a date (`2026-03-19`) or a date with time and time zone (`2026-03-26T08:00:24-07:00`), which Google Sheets does not read as a date. Use `=LEFT(A2,10)` in a new column to get a sortable date.
- **Length** is in seconds (`1299` = 21 min 39 s). In Sheets, `=A2/86400` formatted as **Format > Number > Duration** shows it as `0:21:39`.
- **Comments and titles are other people's text.** A cell that starts with `=`, `+`, `-` or `@` can run as a formula. In Google Sheets, set **Convert text to numbers, dates and formulas** to **No** in the import dialog; in Excel use **Data > From Text/CSV** and keep the text columns as **Text**.

### Input

The form fields, with the name used in the API (JSON) in brackets.

| Form field (API name)                                               | What to enter                                                                                                               | Default           |
| ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| **Video links** (`videos`)                                          | Video links, one per line. Share links, Shorts and live links work.                                                         | one example video |
| **Channels** (`channels`)                                           | Channel addresses or handles, e.g. `https://www.youtube.com/@mkbhd` or `@mkbhd`                                             | empty             |
| **Videos per channel** (`maxVideosPerChannel`)                      | How many of the newest videos to take from each channel                                                                     | 10                |
| **Playlists** (`playlists`)                                         | Playlist links (`https://www.youtube.com/playlist?list=PL...`)                                                              | empty             |
| **Videos per playlist** (`maxVideosPerPlaylist`)                    | How many videos from the top of each playlist                                                                               | 50                |
| **Search words** (`searchQueries`)                                  | One search per line, e.g. `langchain tutorial`                                                                              | empty             |
| **Videos per search** (`maxResultsPerSearch`)                       | How many top videos per search                                                                                              | 10                |
| **Transcript languages** (`languages`)                              | Language codes, best first: `en` English, `es` Spanish, `hi` Hindi, `pt-BR` Portuguese (Brazil)                             | `en`              |
| **Transcript format** (`outputFormat`)                              | Plain text (`text`), Markdown sections (`markdown`), text + timestamped lines (`both`), timestamped lines only (`segments`) | Plain text        |
| **Timestamps in plain text** (`includeTimestamps`)                  | One `[mm:ss] text` line per caption instead of one paragraph                                                                | off               |
| **Extra video details** (`includeMetadata`)                         | Exact publish time, likes, category and @handle (title, channel, views and length always come)                              | on                |
| **Include comments** (`includeComments`)                            | Also get the top comments of each video                                                                                     | off               |
| **Comments per video** (`maxCommentsPerVideo`)                      | How many comments per video (up to 1,000)                                                                                   | 20                |
| **Channel tab** (`channelTab`)                                      | Videos (`videos`), Shorts (`shorts`) or past live streams (`streams`)                                                       | Videos            |
| **Search order** (`searchSort`)                                     | Relevance (`relevance`) or most viewed (`popularity`)                                                                       | Relevance         |
| **Uploaded within** (`searchUploadDate`)                            | Only recent search results: `hour`, `today`, `week`, `month`, `year`                                                        | Any time          |
| **If a video is not in your languages** (`languageFallback`)        | Use the spoken language (`original`), any language (`any`), or skip the video (`none`)                                      | spoken language   |
| **Allow automatic captions** (`includeAutoGenerated`)               | Use YouTube's automatic captions when there are no human-made ones                                                          | on                |
| **Leave out videos without captions** (`skipVideosWithoutCaptions`) | Do not add rows for videos without a transcript                                                                             | off               |
| **Comment order** (`commentsSort`)                                  | Top comments (`top`) or newest first (`newest`)                                                                             | Top               |
| **Markdown section size** (`chunkCharacters`)                       | Rough length of each Markdown section, in characters                                                                        | 1500              |
| **Max videos in total** (`maxItems`)                                | Safety limit across all inputs (videos without captions count too). The old name `maxVideos` still works.                   | 1000              |
| **Parallel videos** (`maxConcurrency`)                              | No need to change                                                                                                           | 10                |
| **Time limit per video** (`videoTimeoutSecs`)                       | No need to change. One attempt at one video may take this long before it is retried                                         | 120               |
| **Retries per video** (`maxRetries`)                                | No need to change. Retries after a rate limit (HTTP 429), timeout or network error, after a growing pause                   | 6                 |
| **Proxy** (`proxyConfiguration`)                                    | Leave as is. Included in the price. Optionally pick a country to scrape as that country (see Known limits).                 | Apify residential |

**The same recipe as JSON** (for the API, n8n or Make):

```json
{
    "channels": ["https://www.youtube.com/@aliabdaal"],
    "maxVideosPerChannel": 50,
    "languages": ["en"],
    "outputFormat": "text",
    "includeComments": true,
    "maxCommentsPerVideo": 5
}
```

**Only Hindi, nothing else (first 5 videos of a playlist):** in the form, set **Transcript languages** to `hi` and **If a video is not in your languages** to **Skip the video**. Otherwise a video spoken in another language (for example Bengali) comes back in that language and is charged like any transcript.

```json
{
    "playlists": ["https://www.youtube.com/playlist?list=PL-CAJsjanTBu7DQydZH8to-Xb3rUO6AoZ"],
    "maxVideosPerPlaylist": 5,
    "languages": ["hi"],
    "languageFallback": "none",
    "outputFormat": "text"
}
```

**Markdown for a ChatGPT or Claude knowledge base (latest 3 videos):**

```json
{
    "channels": ["https://www.youtube.com/@AhrefsCom"],
    "maxVideosPerChannel": 3,
    "outputFormat": "markdown"
}
```

**Search, this month's videos, 30 newest comments each:**

```json
{
    "searchQueries": ["langchain tutorial"],
    "maxResultsPerSearch": 10,
    "searchUploadDate": "month",
    "includeComments": true,
    "maxCommentsPerVideo": 30,
    "commentsSort": "newest"
}
```

### Output

One row per video. The **Overview** tab looks like this (real values from runs on 2026-10-19):

| Position | Title                                                                | Published                 | Views         | Likes      | Length (seconds) | Words | Language                 | Status |
| -------- | -------------------------------------------------------------------- | ------------------------- | ------------- | ---------- | ---------------- | ----- | ------------------------ | ------ |
| 26       | How I'd Create Content in 2026 (If I Had To Start Over)              | 2026-03-26T08:00:24-07:00 | 358,724       | 10,968     | 955              | 4,417 | English (auto-generated) | ok     |
| 27       | The psychology of making money                                       | 2026-03-19                | 136,647       | 4,018      | 1,299            | 5,743 | English (auto-generated) | ok     |
|          | Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster) | 2009-10-24T23:57:33-07:00 | 1,828,063,137 | 19,488,185 | 213              | 366   | English                  | ok     |

The tabs also show the video link, the channel, the transcript (Transcripts tab), each Markdown section with a link to that moment (Markdown tab) and each comment with author, likes and replies (Comments tab, one comment per row).

**Status** tells you what happened to each video:

| Status                   | In plain words                                                                                                  | `errorCode`                                                                                                    | Charged |
| ------------------------ | --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------- |
| `ok`                     | Transcript delivered                                                                                            | `null`                                                                                                         | Yes     |
| `no_captions`            | The video has no subtitles on YouTube, not even automatic ones (or only automatic ones and you turned them off) | `NO_CAPTIONS`, `AUTO_CAPTIONS_EXCLUDED`, `EMPTY_CAPTION_TRACK`                                                 | No      |
| `language_not_available` | The video has subtitles, but not in your languages, and you chose "Skip the video"                              | `LANGUAGE_NOT_AVAILABLE`                                                                                       | No      |
| `unavailable`            | Private, deleted, members-only, age-restricted, not available in the proxy's country, or live right now         | `VIDEO_UNAVAILABLE`, `LOGIN_REQUIRED`, `LIVE_STREAM`, `GEO_RESTRICTED`                                         | No      |
| `error`                  | YouTube refused access (bot check), kept rate-limiting, or the connection failed after several retries          | `BLOCKED`, `RATE_LIMITED`, `CAPTIONS_NEED_PO_TOKEN`, `TIMEOUT`, `NETWORK_ERROR`, `HTTP_ERROR`, `UNKNOWN_ERROR` | No      |

`errorMessage` has the reason in words (YouTube's own text for unavailable videos). A video never comes back as an empty "success": if there is no transcript, `transcriptAvailable` is `false` and `errorCode` says why. If comments fail on a video that has a transcript, the transcript row is still `ok` and `commentsError` explains what happened to the comments.

**Language chosen** (`languageMatch`) shows how the transcript language was picked: `preferred-manual` or `preferred-auto` = one of your languages (human-made or automatic captions), `original` = not in your languages, so the language spoken in the video was used, `any` = the first language the video has (also shown when you chose the spoken language but YouTube cannot tell it, for example a video without automatic captions: then the first human-made captions are used).

One real row in JSON (local run of 2026-10-24, input `{"videos": ["dQw4w9WgXcQ"], "includeTimestamps": true, "includeComments": true, "maxCommentsPerVideo": 5}`; long text and lists shortened, commenter replaced by a placeholder):

```json
{
    "videoId": "dQw4w9WgXcQ",
    "videoUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "status": "ok",
    "errorCode": null,
    "errorMessage": null,
    "title": "Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster)",
    "channelName": "Rick Astley",
    "channelId": "UCuAXFkgsw1L7xaCfnd5JJOw",
    "channelHandle": "@RickAstleyYT",
    "channelUrl": "https://www.youtube.com/@RickAstleyYT",
    "publishedAt": "2009-10-24T23:57:33-07:00",
    "durationSeconds": 213,
    "viewCount": 1829759604,
    "likeCount": 19493269,
    "description": "The official video for “Never Gonna Give You Up” by Rick Astley. ...",
    "keywords": ["rick astley", "Never Gonna Give You Up", "nggyu"],
    "category": "Music",
    "isLive": false,
    "thumbnailUrl": "https://i.ytimg.com/vi/dQw4w9WgXcQ/sddefault.jpg",
    "language": "en",
    "languageName": "English",
    "transcriptType": "manual",
    "transcriptAvailable": true,
    "languageMatch": "preferred-manual",
    "availableLanguages": [
        {
            "code": "en",
            "name": "English",
            "isAutoGenerated": false
        },
        {
            "code": "en",
            "name": "English (auto-generated)",
            "isAutoGenerated": true
        },
        {
            "code": "de-DE",
            "name": "German (Germany)",
            "isAutoGenerated": false
        },
        {
            "code": "ja",
            "name": "Japanese",
            "isAutoGenerated": false
        }
    ],
    "transcriptText": "[00:01] [♪♪♪]\n[00:18] ♪ We're no strangers to love ♪\n[00:22] ♪ You know the rules and so do I ♪\n...",
    "segments": null,
    "segmentCount": 61,
    "chunks": null,
    "wordCount": 366,
    "comments": [
        {
            "commentId": "UgyEnXfdC-umwvTt8JF4AaABAg",
            "text": "Gonna flag this for nudity so I can rick roll the YouTube staff",
            "author": "@example-user",
            "authorChannelId": "UC...",
            "isCreator": false,
            "isPinned": false,
            "likeCount": 569000,
            "replyCount": 723,
            "publishedTimeText": "6 years ago"
        }
    ],
    "commentCount": 2460372,
    "commentsDisabled": false,
    "commentsError": null,
    "source": {
        "type": "video",
        "input": "dQw4w9WgXcQ",
        "position": null
    },
    "collectedAt": "2026-10-23T20:37:27.579Z"
}
```

Rows without a transcript from the same run (other fields shortened):

```json
{"videoId":"AAAAAAAAAAA","status":"unavailable","errorCode":"VIDEO_UNAVAILABLE","errorMessage":"This video is unavailable","transcriptAvailable":false,"transcriptText":null,"segmentCount":null}
{"videoId":"8c7KOGxeY3w","status":"no_captions","errorCode":"NO_CAPTIONS","errorMessage":"video has no captions","transcriptAvailable":false,"transcriptText":null,"segmentCount":null}
```

Every row has the same fields; fields that do not apply to your format are `null`. More examples (timestamped lines, Markdown sections, comments) are in "For developers" below.

### How much does it cost?

You pay only for what you get. Apify platform and proxy costs are included. There is no start fee.

| What                                                        | Price                              |
| ----------------------------------------------------------- | ---------------------------------- |
| Transcript (one video with status `ok`, any length)         | **$2 per 1,000** ($0.002 each)     |
| Comment (one delivered comment)                             | **$0.50 per 1,000** ($0.0005 each) |
| Videos without captions, unavailable, failed or found twice | Free                               |

Examples:

- 1 video: $0.002.
- The first 5 lectures of a playlist: $0.01.
- A channel's last 50 videos with 5 comments each: 50 × $0.002 + 250 × $0.0005 = $0.10 + $0.125 = **$0.225**.
- 100 videos with 20 comments each: $0.20 + $1.00 = **$1.20**.
- 1,000 transcripts: **$2**. 10,000 transcripts: **$20**.
- A 3-hour podcast costs the same as a 3-minute video: $0.002.

Comments are often most of the bill: 5 comments cost more than the transcript of their video. Keep **Comments per video** low if you only want a taste. To cap the bill, open **Run options** (next to the **Start** button) and set **Maximum cost per run**; the Actor stops cleanly when it reaches the limit.

### Integrations

- **Google Sheets:** the simplest way is the recipe above (Export CSV, then File > Import). To send results to a sheet automatically after each run, use the **Integrations** tab of the Actor with a Google Sheets step in Make or Zapier, or the "Google Sheets Import & Export" Actor from Apify Store.
- **Make and Zapier:** add the Apify app, choose **Run an Actor**, pick this Actor, paste a JSON input from above, then add **Get dataset items** and send the rows to Google Sheets, Notion, Airtable or email.
- **n8n:** add an **HTTP Request** node: method `POST`, URL `https://api.apify.com/v2/acts/superxcoder~youtube-transcripts-scraper/run-sync-get-dataset-items`, header `Authorization: Bearer <your Apify API token>`, JSON body like `{"videos": ["{{ $json.videoUrl }}"], "outputFormat": "text"}`. It waits up to 5 minutes and returns one item per video. For bigger jobs use the Apify node's "Run Actor" and "Get dataset items".
- **Claude, ChatGPT, Cursor and other AI agents (MCP):** add `https://mcp.apify.com/?actors=superxcoder/youtube-transcripts-scraper` as an MCP server in Claude Desktop, Claude Code, Cursor or VS Code and sign in with your Apify account. Then ask "get the transcript of this video in Spanish" or "the last 5 videos of @channel as Markdown".
- **API:** see "For developers" below. Your API token is in Apify Console under **Settings > API & Integrations**.

### FAQ

**Is it legal to scrape YouTube transcripts?**
The Actor reads only public data that YouTube shows to any visitor without logging in. It uses no account, cookies or login. Comment rows hold only the author's public name or handle and channel ID. You are responsible for how you use the data: respect YouTube's terms, the copyright in the transcripts, and privacy laws such as GDPR when you store or publish comments. If unsure, ask a lawyer.

**Why did I get empty results or "no_captions"?**
Many videos, especially Shorts, music and non-English lectures, have no subtitles at all, not even automatic ones. Those rows say `no_captions` and are free. `language_not_available` means the video has subtitles, but not in your languages, and you chose to skip such videos. Add more languages, or choose "Use the language spoken in the video".

**I asked for Hindi and got Bengali. Why?**
With the default **If a video is not in your languages: Use the language spoken in the video**, a video without Hindi captions comes back in its own language and is charged. The **Language** and **Language chosen** columns show this. Choose **Skip the video** to get only your languages.

**"Channel not found". What now?**
The handle must match YouTube exactly (for example `@AhrefsCom`, not `@ahrefs`). Open the channel on YouTube and copy the address from your browser. If no channel, playlist or search could be read and there are no video links, the run fails and nothing is charged. If other inputs worked (for example the prefilled example video), the run succeeds and those videos are charged; the log and `RUN-SUMMARY` (`problems`) name the channel that was not found.

**The log shows warnings like "HTTP 429" or "Sign in to confirm you're not a bot". Is it broken?**
No. A rate limit (HTTP 429) is retried after a pause. A bot check is YouTube refusing access to that video: the Actor accepts the refusal, does not retry it, and returns the video as `error` with `errorCode: "BLOCKED"`, free of charge. On Apify this is rare: our cloud tests saw a few bot checks per 100 videos. What counts is the run status (Succeeded) and the **Status** column of each video. Run the blocked videos again later if you need them.

**Why are the rows not in order?**
Videos are fetched in parallel, so rows arrive in any order. Sort by **Position** (1 = newest in a channel, first in a playlist or search) or by **Published**.

**The first comment is the creator's own promo link. Can I skip it?**
Not yet. The creator's pinned comment is usually first in "Top comments" and counts towards **Comments per video**. The Comments tab marks it with **Pinned** and **By creator**, so you can filter it out in your sheet.

**Can it translate transcripts?**
No. YouTube blocks its machine-translated captions for automated tools, so translation is not offered. You get the languages the video really has; translate the text with ChatGPT, DeepL or Google Translate if you need to.

**How do I run it every day or every week?**
In Apify Console, click **Schedule** (or go to **Schedules > Create new**), choose how often, and pick this Actor with your input. Combine it with the Google Sheets or Make integration to get new transcripts automatically.

**Does it work with Shorts and live streams?**
Yes. Paste Shorts links as videos, or set **Channel tab** to Shorts or Past live streams. A stream that is live right now has no transcript yet (`unavailable`).

**Are comment replies included?**
No. You get top-level comments (top or newest first, up to 1,000 per video) with each comment's reply count.

### Known limits

What this Actor can and cannot get, without a YouTube login (it never uses one):

- **Public videos only.** Private, members-only and age-restricted videos need a signed-in account, so they come back as `unavailable` (`LOGIN_REQUIRED` or `VIDEO_UNAVAILABLE`) and are free.
- **Only captions that exist on YouTube.** The Actor never makes up a transcript, never summarises with AI, and never runs speech-to-text. A video without captions is `no_captions` (free). Many Shorts and music videos have none.
- **No translated captions.** YouTube's machine translation (`tlang`) needs a "PO token" that YouTube only gives to its own web player, so it is not offered. You get the languages the video really has.
- **Live streams** have no transcript while they are live (`LIVE_STREAM`); past streams work once YouTube has made their captions.
- **Regions.** YouTube decides availability by the country of the proxy IP. By default no proxy country is set, so Apify picks residential IPs in any country, and a video its uploader limited to some countries can come back `unavailable` with `errorCode: "GEO_RESTRICTED"` (free). The Actor never retries such a video from another country. In our 100-video test, 7 of 100 search results were `GEO_RESTRICTED`. To get the videos available in one country, choose that country under **Proxy** (for example United States); the Actor then uses only that country.
- **Bot checks are not bypassed.** When YouTube asks for a bot check, shows a captcha page or answers HTTP 403, the Actor stops on that video: no retry, no new IP, no other app client, no captcha solving, no login. The video gets a free `error` row with `errorCode: "BLOCKED"`. Likewise, caption tracks that need YouTube's PO token come back as `CAPTIONS_NEED_PO_TOKEN` (free).
- **Up to** 5,000 videos per channel or playlist, 500 per search, 1,000 comments per video (no replies) and 100,000 videos per run.
- **Speed:** about 40 to 50 videos per minute on Apify (measured: 100 videos in 129 to 171 seconds). A run that reaches its **Timeout** stops a few seconds early, keeps every finished row and says how many found videos were left; only finished rows are charged.
- **Search** returns regular videos only and cannot sort by newest; use **Uploaded within** instead.
- **Max videos in total** counts every video the run tries, including free ones without captions. To cap what you pay, use **Maximum cost per run**.
- Comment like counts come from YouTube's short form ("291K"), so large numbers are approximate.
- **YouTube can change.** The Actor uses the same app interfaces as YouTube's iPhone and Android apps. If YouTube starts asking those apps for a PO token too, captions stop working until the Actor is updated; the run then fails with a clear message instead of returning empty rows, and nothing is charged.

### For developers

#### Output formats

- `text`: `transcriptText` is the whole transcript as one string (with `includeTimestamps: true`, one `[mm:ss] text` line per caption).
- `segments`: `segments` is a list of caption lines `{startSeconds, durationSeconds, text}`, in time order.
- `both`: `segments` and `transcriptText`.
- `markdown`: `chunks` is a list of sections of about `chunkCharacters` characters, each ending on a caption line, with a `### [mm:ss] Title (part n)` heading and a link to that moment.

Timestamped lines (format `both` or `segments`):

```json
[
    { "startSeconds": 1.36, "durationSeconds": 1.68, "text": "[♪♪♪]" },
    { "startSeconds": 18.64, "durationSeconds": 3.24, "text": "♪ We're no strangers to love ♪" },
    { "startSeconds": 22.64, "durationSeconds": 4.32, "text": "♪ You know the rules and so do I ♪" }
]
```

Markdown sections (format `markdown`, `chunkCharacters: 1500`, the first of 2, text shortened):

```json
[
    {
        "index": 0,
        "startSeconds": 1.36,
        "endSeconds": 161.32,
        "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=1s",
        "markdown": "### [00:01] Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster) (part 1)\n\n[♪♪♪] ♪ We're no strangers to love ♪ ..."
    }
]
```

One comment (with `includeComments: true`; author shown as a placeholder):

```json
{
    "commentId": "Ugxzbv_ZeZXNRcfwsSF4AaABAg",
    "text": "Is everyone going to ignore the fact that Rick Astley looks like a twelve year old ...",
    "author": "@example-user",
    "authorChannelId": "UC...",
    "isCreator": false,
    "isPinned": false,
    "likeCount": 291000,
    "replyCount": 683,
    "publishedTimeText": "6 years ago"
}
```

Every row has `segmentCount` (caption lines), `transcriptType` (`manual` = uploaded by the creator, `auto_generated` = YouTube's speech recognition) and `transcriptAvailable`, in every format.

The run's key-value store (Storage tab) also holds `RUN-SUMMARY`: how many videos had each status, what was charged, which inputs were invalid, connection statistics, and `reliability` (retries by kind, timeouts, time per video as average/p50/p95/max, videos per minute). Its `status` is `finished`, `failed`, `aborted`, `timed_out` (the run stopped itself just before its Timeout and kept what it had) or `running` (still going, or killed without warning). Dataset views for the API: `?view=overview`, `?view=transcripts`, `?view=markdown`, `?view=comments`.

#### API examples

**curl** (waits for the run and returns the rows):

```bash
curl -X POST "https://api.apify.com/v2/acts/superxcoder~youtube-transcripts-scraper/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"videos": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"], "languages": ["en"], "outputFormat": "text"}'
```

**JavaScript** (`npm install apify-client`):

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('superxcoder/youtube-transcripts-scraper').call({
    channels: ['@mkbhd'],
    maxVideosPerChannel: 10,
    outputFormat: 'markdown',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const video of items) {
    if (video.status === 'ok') console.log(video.title, video.chunks.length, 'chunks');
}
```

**Python** (`pip install apify-client`):

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("superxcoder/youtube-transcripts-scraper").call(
    run_input={
        "videos": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ", "https://youtu.be/jNQXAC9IVRw"],
        "languages": ["en"],
        "outputFormat": "both",
    }
)
for video in client.dataset(run["defaultDatasetId"]).iterate_items():
    if not video["transcriptAvailable"]:
        print(video["videoId"], video["status"], video["errorCode"], video["errorMessage"])
        continue
    print(video["title"], video["transcriptType"], video["segmentCount"], "lines")
    for seg in video["segments"][:3]:
        print(f'  {seg["startSeconds"]:>7.2f}s  {seg["text"]}')
```

#### AI and RAG pipelines

Choose `outputFormat: "markdown"`: each video comes as sections of about `chunkCharacters` characters that end on a caption line, each with a `[mm:ss]` heading and a link to that moment. Embed the sections as they are and keep the link as the citation. The Actor makes no LLM calls and adds no AI text: what you embed is YouTube's caption text.

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("superxcoder/youtube-transcripts-scraper").call(
    run_input={"channels": ["@lexfridman"], "maxVideosPerChannel": 5, "outputFormat": "markdown", "chunkCharacters": 1500}
)
documents = [
    {
        "id": f'{video["videoId"]}-{chunk["index"]}',
        "text": chunk["markdown"],
        "metadata": {
            "source": chunk["url"],  # deep link, e.g. ...watch?v=ID&t=754s
            "title": video["title"],
            "channel": video["channelName"],
            "language": video["language"],
            "startSeconds": chunk["startSeconds"],
        },
    }
    for video in client.dataset(run["defaultDatasetId"]).iterate_items()
    if video["transcriptAvailable"]
    for chunk in video["chunks"]
]
## documents -> your embedding model and vector store (pgvector, Pinecone, Chroma, ...)
```

Transcripts are third-party text: keep them in the "context" part of a prompt, not the instructions.

#### n8n

1. Add an **HTTP Request** node: method `POST`, URL `https://api.apify.com/v2/acts/superxcoder~youtube-transcripts-scraper/run-sync-get-dataset-items`, **Authentication: Generic > Header Auth** with name `Authorization` and value `Bearer <your Apify API token>`.
2. **Body** (JSON): `{"videos": ["{{ $json.videoUrl }}"], "languages": ["en"], "outputFormat": "text"}`. One item comes back per video, with `transcriptText`, `title`, `status` and `errorCode`.
3. Add an **IF** node on `{{ $json.transcriptAvailable }}`, then send the true branch to OpenAI, Google Sheets or Notion.

The sync endpoint waits up to 5 minutes, enough for about 100 videos. For bigger batches use the **Apify** community node: **Run an Actor** (wait for finish), then **Get dataset items**.

#### Technical notes

- **Blocked IPs.** YouTube blocks caption requests from most cloud and datacenter IPs, which is why `youtube-transcript-api` scripts that work on a laptop fail on a server, in Colab or in n8n cloud ("IP blocked", "RequestBlocked"). The Actor asks YouTube's mobile app API (Innertube, iOS first, then Android) for the caption tracks, sends the player and caption requests through residential proxy, and moves to the next app client only when one answers with an error that is not an access refusal (for example an app version YouTube no longer accepts). A bot check, captcha page or HTTP 403 ends that video as `BLOCKED` without a retry. Channel, playlist, search and comment pages go through cheaper datacenter proxy.
- **Translation.** YouTube's machine-translated caption tracks (`tlang`) answer HTTP 429 to every request without a PO token, from home, datacenter and residential IPs alike, so they are not offered.
- **Spoken language.** With `languageFallback: "original"`, the language of YouTube's automatic speech-recognition track decides the spoken language, then a human-made track in that language is preferred. When YouTube cannot tell (no automatic captions), the first human-made captions are used and the row says `languageMatch: "any"`. Many tools take the first track, which is often a different language.
- **Language codes** follow BCP-47: legacy codes are converted (`iw` becomes `he`, `in` becomes `id`). `en` matches `en-US` and `en-GB`; `pt-BR` falls back to `pt`. A video can list the same code twice when it has two human-made tracks in one language.
- **Rate limits and speed.** Requests are spread over many IPs and sessions. Measured on Apify: 100 videos took 129 to 150 seconds at `maxConcurrency` 10 and 171 seconds at 30, so 10 (the default) is the sweet spot.
- **Retries and time limits.** A video that is rate-limited (HTTP 429), times out, gets a 5xx or hits a network error is retried up to `maxRetries` times after an exponential pause with random jitter (1-30 s after a 429, 0.5-8 s otherwise). Access refusals are never retried: a bot check, captcha page or 403 gives `BLOCKED`, a PO-token caption track gives `CAPTIONS_NEED_PO_TOKEN`, a region block gives `GEO_RESTRICTED`. Every HTTP request has a hard deadline (at most 30 s) and every attempt at a video a time limit (`videoTimeoutSecs`), so nothing can hang. One failed video never stops the others: it gets its own `error` row.
- **publishedAt** is exact (ISO 8601 with time zone) when YouTube's web player response is returned, otherwise the date from the watch page (`YYYY-MM-DD`).
- **Region.** Requests ask YouTube for US content (`gl=US`, which affects search ranking), but availability follows the proxy IP's country. No country is pinned by default; a region-blocked video is reported once as `GEO_RESTRICTED` and never retried from another country. Pin a country under **Proxy** to scrape as that country.
- **Fails loud.** If YouTube blocks every request, the proxy is unreachable, every channel or playlist is unknown, or no video lists any caption track, the run fails with a clear message and nothing is charged, instead of succeeding with an empty dataset.
- **Untrusted content.** Titles, captions and comments are written by other people. Treat them as untrusted input in LLM prompts (they can contain instructions or links). Markdown headings strip markdown control characters from titles. In Google Sheets or Excel, a cell that starts with `=`, `+`, `-` or `@` can run as a formula, so keep **Convert text to numbers, dates and formulas** off when importing, and do not allow external links from imported comments.

# Changelog

This Actor's version history is a separate document: https://apify.com/superxcoder/youtube-transcripts-scraper/changelog.md

# Actor input Schema

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

Paste YouTube video links, one per line. Example: https://www.youtube.com/watch?v=dQw4w9WgXcQ. Share links (youtu.be/...), Shorts and live links work too. The example video is filled in so you can try the Actor at once: remove it if you only want channels, playlists or searches, or you also get (and pay for) its transcript.

## `channels` (type: `array`):

Get the newest videos of a channel. Easiest: open the channel on YouTube and copy the address from your browser, e.g. https://www.youtube.com/@mkbhd. A handle like @mkbhd also works, but it must be spelled exactly as on YouTube.

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

How many of the newest videos to take from each channel. Example: 50 for a channel's last 50 videos.

## `playlists` (type: `array`):

Paste playlist links, one per line. Example: https://www.youtube.com/playlist?list=PL... (open the playlist on YouTube and copy the address). Videos are taken in playlist order.

## `maxVideosPerPlaylist` (type: `integer`):

How many videos to take from each playlist, from the top. Example: 5 for the first 5 lectures.

## `searchQueries` (type: `array`):

Search YouTube and get transcripts of the top videos. One search per line. Example: langchain tutorial. Only regular videos are returned (no channels, playlists or ads).

## `maxResultsPerSearch` (type: `integer`):

How many videos to take from each search, in YouTube's order. Example: 10.

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

Two-letter language codes, best first, one per line. Examples: en = English, es = Spanish, hi = Hindi, de = German, pt-BR = Portuguese (Brazil). "en" also matches US and UK English. Human-made captions are used before automatic ones. Translation is not offered, because YouTube blocks translated captions.

## `outputFormat` (type: `string`):

Plain text gives one transcript cell per video. Markdown splits each transcript into sections, each with a \[mm:ss] time heading and a link to that moment in the video (see the Markdown tab). Timestamped lines give every caption line with its start time and length in seconds.

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

Start each caption line of the plain text with its time, like "\[01:23] text", one line per caption. Only for the plain-text formats. Off: the plain text is one paragraph.

## `includeMetadata` (type: `boolean`):

Also get the exact publish time, like count, category and channel @handle. Title, channel, duration, views, description, keywords and thumbnail always come with the video. Turn off for slightly faster runs when you only need the text.

## `includeComments` (type: `boolean`):

Also get the top comments of each video that has a transcript. Each comment costs extra (see Pricing). Videos without a transcript get no comments and cost nothing.

## `maxCommentsPerVideo` (type: `integer`):

How many comments to get per video (replies are not included). Example: 5. The creator's own pinned comment, often a promo link, is usually the first one and counts towards this number.

## `channelTab` (type: `string`):

Which list of a channel to read. Example: Shorts to get a channel's latest Shorts.

## `searchSort` (type: `string`):

Order of search results. YouTube no longer offers "newest first" for search; use "Uploaded within" for recent videos.

## `searchUploadDate` (type: `string`):

Only search results uploaded within this period. Example: This month.

## `languageFallback` (type: `string`):

With the first two options you may get a transcript in a different language than you asked for (e.g. Bengali when you asked for Hindi), and it is charged like any transcript. "Spoken language" uses YouTube's automatic captions to tell the spoken language; when a video has none, the first human-made captions are used, which can be any language. The "Language chosen" column shows when this happened. Choose "Skip" if you only want your languages.

## `includeAutoGenerated` (type: `boolean`):

Use YouTube's automatic (speech recognition) captions when there are no human-made ones. Most videos only have automatic captions. Turn off to get only human-made captions; other videos then come back as "no captions" and are not charged.

## `skipVideosWithoutCaptions` (type: `boolean`):

Do not add rows for videos that have no usable captions. These videos are never charged either way. They still count towards "Max videos in total".

## `commentsSort` (type: `string`):

Which comments to take first. Example: Newest first.

## `chunkCharacters` (type: `integer`):

Rough length of each Markdown section. Only used with the Markdown format. Example: 1500 (about a page of text, or 2 minutes of speech).

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

Safety limit: stop after this many videos across all inputs. Example: 100. Videos without captions count too. To cap what you pay, set "Maximum cost per run" in Run options, next to the Start button. (Older tasks that use "maxVideos" still work.)

## `maxConcurrency` (type: `integer`):

No need to change. How many videos to fetch at the same time. Example: 10. Higher numbers are rarely faster.

## `videoTimeoutSecs` (type: `integer`):

No need to change. Longest time one attempt at one video may take before it is retried. Each web request inside it is limited to a third of this (at most 30 s). Comment fetching gets extra time on top. Example: 120.

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

No need to change. How many times a video (or a channel, playlist or search page) is tried again after a rate limit (HTTP 429), timeout or network error, after a short, growing pause. A bot check is not retried. Videos that still fail get a row with status "error" and are not charged. Example: 6.

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

Leave as is. The default (Apify residential proxy) is included in the price, so it costs you nothing extra. Changing it can make YouTube block the captions. Pick a country to get the videos available there; with no country, a region-limited video can come back GEO_RESTRICTED (free).

## Actor input object example

```json
{
  "videos": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
  ],
  "channels": [],
  "maxVideosPerChannel": 10,
  "playlists": [],
  "maxVideosPerPlaylist": 50,
  "searchQueries": [],
  "maxResultsPerSearch": 10,
  "languages": [
    "en"
  ],
  "outputFormat": "text",
  "includeTimestamps": false,
  "includeMetadata": true,
  "includeComments": false,
  "maxCommentsPerVideo": 20,
  "channelTab": "videos",
  "searchSort": "relevance",
  "searchUploadDate": "",
  "languageFallback": "original",
  "includeAutoGenerated": true,
  "skipVideosWithoutCaptions": false,
  "commentsSort": "top",
  "chunkCharacters": 1500,
  "maxItems": 1000,
  "maxConcurrency": 10,
  "videoTimeoutSecs": 120,
  "maxRetries": 6,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

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

No description

## `markdown` (type: `string`):

No description

## `comments` (type: `string`):

No description

## `all` (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 = {
    "videos": [
        "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
    ],
    "channels": [],
    "playlists": [],
    "searchQueries": [],
    "languages": [
        "en"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("superxcoder/youtube-transcripts-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 = {
    "videos": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],
    "channels": [],
    "playlists": [],
    "searchQueries": [],
    "languages": ["en"],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("superxcoder/youtube-transcripts-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 '{
  "videos": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
  ],
  "channels": [],
  "playlists": [],
  "searchQueries": [],
  "languages": [
    "en"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call superxcoder/youtube-transcripts-scraper --silent --output-dataset

```

## MCP server setup

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