# YouTube Transcript Scraper & API: Captions, Subtitles, Shorts (`quarra/youtube-transcript-scraper`) Actor

Bulk YouTube transcripts from videos, Shorts, channels, playlists and searches, in the language spoken. Text with timestamps, SRT and VTT, chapters, exact comment count and subscribers, and a watch mode that returns only new videos. Videos without captions are not charged.

- **URL**: https://apify.com/quarra/youtube-transcript-scraper.md
- **Developed by:** [Quarra Labs](https://apify.com/quarra) (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 $5.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.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## YouTube Transcript Scraper: Videos, Shorts, Channels, Playlists, Search

Get YouTube transcripts (subtitles and closed captions) in bulk, without a YouTube account or API key. Give it video URLs,
Shorts, channels, playlists or search terms; each video comes back with its full transcript as plain text ready for
AI and LLM pipelines, as timestamped segments and, if you want, as SRT and VTT subtitles, with the language you asked
for, the list of every caption track available, chapters and the video's details (views, likes, exact comment
count, thumbnail, channel subscribers). Export as JSON, CSV or Excel (see [Spreadsheets](#csv-and-excel)), or read it
through the Apify API. **$5 per 1,000 transcripts**, plus a small start fee per run; videos without captions and errors
are free.

Three things you would otherwise build yourself: a whole channel or playlist in one run, a monitoring mode that
returns only the videos you have not received yet, and a free record that says exactly why a video has no
transcript.

### Quick start

1. Paste a video, Short, channel or playlist link (or `@handle`) into **Videos, channels or playlists**, or words into
   **Search queries**.
2. Click **Start**.
3. Read the transcripts in the run's **Output** tab, or export them as JSON, CSV or Excel.

Like any Apify Actor, it can also be run and read through the Apify API, scheduled, and connected to webhooks, Make,
Zapier, n8n or Apify's MCP server for AI agents.

### What you can transcribe

| Input | Examples | What you get |
|---|---|---|
| Videos | `https://www.youtube.com/watch?v=8jPQjjsBbIc`, `https://youtu.be/8jPQjjsBbIc`, `8jPQjjsBbIc` | that video |
| Shorts, live, embed | `https://www.youtube.com/shorts/…`, `/live/…`, `/embed/…` | that video |
| Channels | `@TED`, `https://www.youtube.com/@TED/shorts`, `https://www.youtube.com/channel/UC…` | its latest videos, Shorts, past streams or all uploads (`channelContent`, or the tab in the URL), newest first |
| Playlists | `https://www.youtube.com/playlist?list=PL…`, `PL…` | the playlist's videos in order |
| Search | `machine learning lecture` in `searchQueries` | the videos YouTube returns, by relevance or popularity (`searchSort`) |

Mix them freely in one run. `maxVideosPerSource` caps each channel, playlist and search (default 10, up to 10,000).
A video that appears in two of your sources is transcribed and charged once, and still counts in the window of each
source, so no source ever returns more than `maxVideosPerSource` videos. A channel given twice under different forms
(`@TED`, its `/channel/UC…` URL, `/c/TED`, `/user/…`) is read once per tab: one window, not two.

A `@handle` is checked against the channel YouTube leads to, the way YouTube compares handles (case, accents and
character width do not matter: `@ChefAmelie` is `@ChefAmélie`); if that channel carries another handle, you get a
free `channel_not_found` record, never another channel's videos. Handles in any script work (`@বাংলা`,
`@ChefAmélie`). When YouTube leads a handle to a channel written differently, the record says which
channel it actually is. Lists are read page after page to their end; if YouTube ever stops returning the next page,
the run says so in its status and in `OUTPUT` (`listIncomplete`) instead of pretending the channel is shorter.

Channels come newest first. With `channelSort: popular` (or `?sort=p` in a tab URL) the Videos and Shorts tabs come
most viewed first, all the way down the channel. Combined with `publishedAfter`, the Videos tab is read from the most
recent video back to your date and those videos are ranked by views (view counts as shown in YouTube's lists,
rounded); the Shorts tab has no dates in YouTube's lists, so Shorts then come newest first, reading stops at the
first Short older than your date, and the run says so. `@name/live` gives the video of the channel's live stream in
progress.

### What we measured

On the Apify platform on 9 October 2026, at the default memory (512 MB) unless stated:

- **62 videos** taken from a channel (@TED), four searches in three languages (English, French, Spanish), a Short
  and a video without captions: 57 transcripts, and 5 free `missing` records with the reason (3 without any
  captions, 1 whose captions held no speech, 1 live stream still in progress). Where YouTube marks the original audio track (46 of the 57), the transcript is in
  that language.
- 62 videos in 77 seconds, three of them tutorials of 12 to 16 hours.
- Three lectures that YouTube's AI has dubbed into 11 to 19 languages: all three come back in English, their original
  language.
- 12 videos of a podcast channel, up to 5 h 16 min each (39.5 hours of speech), with SRT and VTT: 12 of 12 in
  89 seconds with 256 MB, the smallest memory this Actor accepts (the run peaked at 134 MB).
- The default input (one video) runs in about 4 seconds.
- A run of 15 videos restarted by the platform in the middle (we forced it after 6 records) finished with 15 records
  for 15 videos: none repeated, and the transcripts counted for billing match the transcripts delivered (14 of 14).

A video with captions comes back with its transcript, or with a free record that says why; a run never "succeeds"
with empty output when the failure is on our side or YouTube's. Every record says which language you got, whether it
was the one you asked for (`requestedLanguageFound`), and lists all the tracks of the video (`availableLanguages`).

### Languages

- Leave `languages` empty to get each video in the language spoken in it: the creator's captions in that language
  if they exist, otherwise YouTube's automatic captions.
- Or give a list in order of preference (`["en", "es"]`). For each language, captions written by the creator come
  first, then automatic captions. `en` also matches `en-US` and `en-GB`.
- If none of your languages exists on a video, you get the spoken language with `requestedLanguageFound: false`.
  Turn off `useAnyLanguage` to get a free `missing` record instead, with the list of available languages in the
  reason.
- "The language spoken" is the video's original audio track as YouTube marks it (`originalLanguage`). Many videos
  are now dubbed by YouTube's AI into a dozen languages, each with its own automatic captions: those tracks are
  machine translations, so we never return them, neither as the spoken language nor as a language you asked for.
  They are listed in `availableLanguages` with `isAutoDubbed: true`.
- Codes are read the way you type them: `en_US`, `pt-BR`, `he` (YouTube's `iw`), or names such as `English`. A code
  we cannot read is ignored and said in `OUTPUT.inputNotes`; the rest of the run goes on.
- We do not offer translation: YouTube's machine translation is too unreliable for us to charge for. Ask for a
  language and you get the video's own captions in it when they exist; otherwise the rules above apply.

### Output

One record per video. A transcript (`status: "ok"`), shortened here:

```json
{
  "status": "ok",
  "videoId": "8jPQjjsBbIc",
  "url": "https://www.youtube.com/watch?v=8jPQjjsBbIc",
  "title": "How to stay calm when you know you'll be stressed | Daniel Levitin | TED",
  "channel": {
    "id": "UCAuUUnT6oDeKwE6v1NGQxug",
    "name": "TED",
    "handle": "@TED",
    "url": "https://www.youtube.com/@TED",
    "subscriberCount": 27900000,
    "thumbnailUrl": "https://yt3.ggpht.com/ytc/AIdro_koIFcCOrvh0KThLNOiazAIDu6hcs8bjkGNwe1f6A_OYm8=s800-c-k-c0x00ffffff-no-rj"
  },
  "publishedAt": "2015-11-23T16:58:55Z",
  "durationSeconds": 740,
  "viewCount": 20051236,
  "likeCount": 268984,
  "commentCount": 4076,
  "commentsDisabled": false,
  "thumbnailUrl": "https://i.ytimg.com/vi/8jPQjjsBbIc/maxresdefault.jpg",
  "category": "Science & Technology",
  "isShort": false,
  "isLive": false,
  "isLiveNow": false,
  "isUnlisted": false,
  "isFamilySafe": true,
  "originalLanguage": "en",
  "language": "en",
  "languageName": "English",
  "isAutoGenerated": false,
  "requestedLanguageFound": null,
  "availableLanguages": [
    { "languageCode": "ar", "name": "Arabic", "isAutoGenerated": false, "isAutoDubbed": false },
    { "languageCode": "en", "name": "English", "isAutoGenerated": false, "isAutoDubbed": false },
    { "languageCode": "en", "name": "English (auto-generated)", "isAutoGenerated": true, "isAutoDubbed": false }
  ],
  "chapters": null,
  "transcript": "A few years ago, I broke into my own house. I had just driven home, it was around midnight in the dead of Montreal winter, …",
  "segments": [
    { "start": 13.24, "end": 15.8, "duration": 2.56, "text": "A few years ago, I broke into my own house." },
    { "start": 16.88, "end": 18.096, "duration": 1.216, "text": "I had just driven home," }
  ],
  "segmentCount": 260,
  "wordCount": 2232,
  "characterCount": 12360,
  "billedTranscripts": 1,
  "missingReason": null,
  "errorCode": null,
  "error": null,
  "retryable": null,
  "warning": null,
  "input": "https://www.youtube.com/watch?v=8jPQjjsBbIc",
  "inputType": "video",
  "position": null,
  "scrapedAt": "2026-10-07T19:03:48.620Z"
}
```

The real record lists the 41 caption tracks of this video. `position` and `warning` are always there (null here);
`srt`, `vtt`, `description` and `keywords` appear when you ask for them, `truncatedFields` only when it applies. A video
without captions comes back as a free record that holds the reason and the tracks it does have, and no video details
(no title, counts or description: those come with transcripts):

```json
{
  "status": "missing",
  "videoId": "NsPw4HIDK_k",
  "url": "https://www.youtube.com/watch?v=NsPw4HIDK_k",
  "availableLanguages": [],
  "missingReason": "no captions on this video",
  "errorCode": null,
  "error": null,
  "retryable": null,
  "warning": null,
  "input": "NsPw4HIDK_k",
  "inputType": "video",
  "position": null,
  "scrapedAt": "2026-10-09T19:24:26.093Z"
}
```

#### Fields that need a word of explanation

- `status`: `ok` (transcript, charged), `missing` (the video exists but there is no transcript you can get: no
  captions, none in your languages, live stream in progress, premiere not started; free) or `error` (private,
  deleted, age-restricted or members-only video, unknown channel, unreadable line, or YouTube not serving the page; free).
- `segments`: times in seconds, as numbers. Automatic captions on YouTube scroll, so each line stays on screen under
  the next one; we trim `duration` so that segments do not overlap, which gives clean subtitle files. Some manual
  captions carry every line twice (same start, same duration, same text: an outline effect on screen); each such
  line is kept once, while a line really said twice (at another time) stays.
- `commentCount`: the exact number of comments, as the video's comment section counts it; `null` with
  `commentsDisabled: true` when comments are turned off (videos made for kids, for example). `channel.subscriberCount`
  is rounded the way YouTube shows it (27.9M gives 27900000) and `null` if the channel hides it or the video's page
  does not show it (videos uploaded by a VEVO channel, for example). `thumbnailUrl` is the largest thumbnail the video
  has. Rarely, the video's page cannot be read: the comment count, subscriber count and channel avatar are then
  `null` (`commentsDisabled` too), and the transcript is delivered and charged as usual.
- `transcriptFormat`: `both` (default), `text` or `segments`. `includeSrt` and `includeVtt` add the `srt` and `vtt`
  fields: the full text of a ready-to-save `.srt` or `.vtt` file.
- `truncatedFields`: rare. When a very long video's record would exceed Apify's size limit for one record (about
  9 MB), `vtt`, then `srt`, then `segments` are left empty and listed here; `transcript` is always complete.
- `removeSoundTags` removes `[Music]`, `[Applause]`, `(laughter)` and similar tags, handy before an LLM.
- `chapters`: the chapters the creator declared in the description, with YouTube's own rules (first one at 0:00,
  at least three, each at least 10 seconds). Null when there are none.
- `isShort`: true for a Short. `isLive`: a live stream, past or present (not a premiere); `isLiveNow`: still live.
- `wordCount` splits on spaces; for Chinese, Japanese or Thai, use `characterCount`.
- `billedTranscripts`: the transcripts charged for this record: 1, or one per 10 hours started for a video over
  10 hours (see Pricing); 0 for a free re-delivery.
- `missingReason` also covers a past live stream without a recording, captions that hold only sound tags such as
  `[Music]` with a few stray letters, automatic captions with fewer than 5 spoken characters in all (a Chinese,
  Japanese or Korean character counts for three; captions written by the creator are never refused for being
  short), all free with or without `removeSoundTags`, and videos skipped by `excludeMusicVideos`. Speech is measured
  in letters, not words, so Japanese, Chinese or Thai sentences without spaces count fully.
- `errorCode` and `retryable` (all error records are free):
  - final (`retryable: false`): `private_video`, `video_unavailable`, `age_restricted`, `members_only`,
    `geo_restricted` (the uploader has restricted this video by country, so it cannot be read), `paid_video`, `channel_not_found`, `channel_empty`, `no_live_now`, `playlist_not_found`,
    `no_results`, `invalid_url`, `bad_input` (the input as a whole cannot run);
  - worth retrying later (`retryable: true`): `blocked` and `network` (YouTube did not serve the page, or did not answer),
    `login_required`, `content_check` and `unplayable` (YouTube asks for sign-in or a confirmation, or does not play
    the video at that moment), `site_changed`, `listing_failed` and `unexpected` (YouTube changed or failed to serve
    a page, or an error on our side), `watch_busy` (see Only new videos);
  - `captions_too_large`: a caption track far larger than any real track for a video of that length, typically junk
    uploaded on purpose; it is never delivered cut short and not charged (`retryable: false`);
  - `long_video_over_budget`: a video over 10 hours that what was left of the run's maximum charge or `maxItems`
    did not cover; not delivered, not charged (`retryable: true`, with a higher limit);
  - `run_cost_limit`: a video not read because the run reached its limit for work that gives no transcript (see
    Pricing); not charged; `retryable: true` when a new run can read it, `false` when its captions are far larger
    than speech of that length can produce.
- Input lines that are not understood get one `invalid_url` record each, up to 10, then one record that counts the
  rest; `OUTPUT.badInputs` lists the first 100 and `OUTPUT.badInputCount` gives the total. The other lines of the
  input run normally.
- `warning`: rare; something the record could not include, said plainly (for example YouTube did not return a
  video's date and category).
- `includeDescription` adds `description` and `keywords` (the video's tags).
- `input` and `inputType` tell which of your inputs produced the record; `position` is the video's rank in its
  channel, playlist or search (1 = first, newest first for channels). Records are written as soon as each video is
  done, so sort by `input` and `position` to get the source's exact order.

#### CSV and Excel

A transcript is long and `segments` holds hundreds of lines per video, so the full record does not fit a
spreadsheet: Apify's CSV and Excel exports stop at 2,000 columns (checked on 9 October 2026). For a spreadsheet,
export the **Transcripts** view (one row per video: ID, title, language, text, URL, input, position), or run with
`transcriptFormat: "text"`.

Titles, transcripts and handles come from YouTube as their authors wrote them; we do not rewrite them. A cell that
starts with `=`, `+`, `-` or `@` (a channel handle, for example) may be read as a formula when a CSV file is
opened in Excel or Google Sheets. Apify's **Excel** export writes such cells as text, not as formulas (checked on
9 October 2026), so prefer it to the CSV for a spreadsheet. Excel cuts any cell after 32,767 characters (roughly
6,000 words, or half an hour of speech): for longer videos, use JSON, which keeps the whole text.

The dataset has three views (Overview, Transcripts, Video details). The run's summary is the `OUTPUT` record of the
default key-value store: per input, the channel or playlist it resolved to, videos listed, transcripts, missing,
errors, and videos skipped because they were already delivered or published before your date.

### Only new videos (monitoring)

Turn on `onlyNewVideos`, schedule the Actor (hourly, daily), and each run returns only the videos that earlier runs
did not deliver for the same channel, playlist or search. Nothing is charged twice. A run with nothing new ends as
Succeeded with "Nothing new since the last run of this watch" and charges only the start fee.

- **Window.** For channels, uploads playlists (`UU…`) and searches, `maxVideosPerSource` is the window checked at
  each run: with 10, the 10 latest videos are looked at, and the ones already delivered are skipped without being
  downloaded again. When nothing is new, a run reads one page per channel. If a source got more new videos than the
  window since the last run, the run delivers the latest ones and says so; the next runs go on with the older new
  videos, so nothing in between is lost.
- **Playlists** are checked in full at each run (first 2,000 videos; YouTube adds new videos at the end of most
  playlists), and up to `maxVideosPerSource` new videos are delivered per run. **Searches**: add `publishedAfter`
  (for example `3 days`) to keep the watch on recent videos.
- **Videos without captions yet** are checked again after 6 hours, then 12, then every 24 hours, for 3 days
  (YouTube adds automatic captions some time after upload). You get one free `missing` record; re-checks that still
  find nothing add no record. **Live streams and premieres** are followed until they end; their transcript comes in
  the first run that checks them after the end.
- **Memory.** The memory is a key-value store named `quarra-youtube-watch` in your own Apify account (video IDs, no
  transcripts). Without `watchName`, a watch is identified by its channels, playlists and searches; set `watchName`
  to keep one memory while you change the sources, and always for a watch made only of video links. If a run is
  killed, the next one delivers what was missing without charging twice; the status says what happened.
- **Limits.** One run per watch at a time (a second one ends with a free `watch_busy` record); up to 500 sources per
  watch. A run with nothing new checks a limited number of sources: for a watch of hundreds of channels, schedule it
  more often or split it into several watches (`watchName`). The status always says how many sources were not
  checked; the next run starts with them.

Limits in [Only new videos: limits](#only-new-videos-limits) at the end of this page.

### Dates

`publishedAfter` takes a date (`2026-10-01`), a date and time in UTC (`2026-10-01T08:00:00Z`) or a duration
(`7 days`, `2 weeks`, `24 hours`). Older videos are skipped before their transcript is downloaded and are never
charged. For channels, reading stops at the first older video. For searches, YouTube's own upload-date filter is
applied as well, so you get recent videos rather than none.

### Input examples

The latest 20 videos of two channels, in English when available:

```json
{
  "urls": ["@TED", "https://www.youtube.com/@veritasium"],
  "maxVideosPerSource": 20,
  "languages": ["en"]
}
```

A daily watch of a channel and a search, text only, for an LLM pipeline:

```json
{
  "urls": ["@TED"],
  "searchQueries": ["machine learning lecture"],
  "maxVideosPerSource": 25,
  "onlyNewVideos": true,
  "watchName": "ml-daily",
  "publishedAfter": "3 days",
  "transcriptFormat": "text",
  "removeSoundTags": true
}
```

Subtitle files for a video and a Short:

```json
{
  "urls": ["https://youtu.be/8jPQjjsBbIc", "https://www.youtube.com/shorts/QyVlUmluLhs"],
  "transcriptFormat": "segments",
  "includeSrt": true,
  "includeVtt": true
}
```

From your own code, with the [Apify client for Python](https://docs.apify.com/api/client/python) (the JavaScript
client works the same way):

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("quarra/youtube-transcript-scraper").call(
    run_input={"urls": ["@TED"], "maxVideosPerSource": 5, "transcriptFormat": "text"}
)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["status"], item["videoId"], (item.get("transcript") or "")[:80])
```

### Pricing

Pay per result: **$5 per 1,000 transcripts** ($0.005 each, the same on every Apify plan), plus a **start fee of
$0.005 per GB of memory per run** (1 GB minimum: $0.005 at the default 512 MB, $0.02 at 4 GB). A video over 10 hours
counts as one transcript per 10 hours started (a 24-hour live replay counts 3; without a duration from YouTube, the
length of its captions counts; `billedTranscripts` in each record says how many). Videos without captions, errors
and duplicates are free. Platform usage is included. A run that fails before its first transcript charges only its
start fee; transcripts delivered before a failure stay in your dataset and are charged once.

Your maximum charge per run is never exceeded: the run stops at the last transcript it can charge and says so in its
status; a long video that would go over it is not delivered and not charged, and its record says why. `maxItems`
caps the charged transcripts of a run: ask for 10, get 10 (plus the free records met on the way).

Videos without captions are free, so a run cannot read them without limit: when it keeps meeting videos that give
no transcript (no captions, another language, failed inputs, lists read for nothing, re-checks), it stops early and
says why. A list that opens with a long run of videos without captions can stop before delivering anything (in our
tests, after a handful), so split such lists. A run that keeps finding transcripts goes on. One run takes up to
20,000 URLs, 1,000 searches and 20 languages.

| Run (default memory) | Price |
|---|---|
| 1 video | $0.010 ($0.005 start + $0.005) |
| A channel's latest 100 videos, 8 of them here and there without captions | $0.465 |
| A 24-hour live replay | $0.020 ($0.005 start + 3 × $0.005) |
| 1,000 transcripts in one run | $5.005 |

Apify's free plan includes $5 of usage a month, with no credit card required (apify.com/pricing, checked on 9 October
2026\): enough to try this Actor on your own videos.

### Use cases

- Feed transcripts to an LLM or a RAG index: summaries, Q\&A over a channel, course notes.
- Research and media monitoring: what a set of channels or a topic said this week, in their own words.
- Content repurposing: blog posts, newsletters, quotes and timestamps from your own videos.
- Accessibility and localization: subtitle files to edit, translate or re-upload.
- Dataset building for analysis: language, length, topics across thousands of videos.

### Limits

- **Songs.** For music videos, YouTube's automatic captions transcribe the lyrics as if they were speech; they come
  back as transcripts and are charged like any other. We found no rule that tells a song from a talk with certainty,
  so the choice is yours: `excludeMusicVideos` skips every video YouTube files under Music, before its transcript is
  downloaded and without charge, with a free `missing` record (a lesson or interview filed under Music is skipped
  too; a song filed elsewhere, under Entertainment for instance, is not). Captions made only of sound tags such as
  `[Music]` are never charged.
- **No captions, no transcript.** In a sample of 150 varied videos we drew from YouTube searches in 12 languages
  (7 October 2026), 19% had no captions at all (silent Shorts, live streams, music). This Actor reads captions; it
  does not transcribe audio. Those videos come back as free `missing` records.
- **Automatic captions** are what YouTube generates: punctuation is often sparse and speakers are not named.
- Age-restricted and members-only videos need a signed-in account, and paid videos a purchase; we do not use
  accounts, so they come back as free `error` records.
- Live streams have no transcript until they end, and premieres none until they have aired.
- A list with few captions can stop early (see Pricing): split it.
- No translation (see Languages). Comments are counted, not collected.
- Chapters are the ones declared in the description; YouTube's automatic chapters are not included.
- A search inside a channel (`@name/search?query=…`) and a channel's playlist, community or podcast pages are not
  supported: they come back as a free `invalid_url` record. `@name/live` gives the channel's current live stream.
- Search results are the ones YouTube shows a signed-out visitor with its interface in English; a search can return
  fewer videos than asked for. With `publishedAfter`, YouTube's results often hold older videos: the Actor reads up
  to 5 result pages per 20 videos asked, and if that was not enough, the run says so (`listIncomplete` in `OUTPUT`).
- A run lasts at most its timeout (1 hour by default for this Actor). For a channel of thousands of videos, raise the
  timeout or cap the run with `maxItems`; a run that reaches its timeout keeps the transcripts it delivered and
  says so.

### FAQ

**Is it legal to scrape YouTube transcripts?** This Actor reads only publicly available data: the captions YouTube
shows to any visitor who is not signed in. It does not access private, members-only or age-restricted videos.
Transcripts reproduce what people say, which may be protected by copyright and may contain personal data. You are
responsible for making sure your use (research, analysis, quotation, accessibility) complies with the laws and terms
that apply to you, including data-protection rules such as the GDPR in Europe; if in doubt, consult a lawyer. We
do not receive or store your results: they stay in your Apify account, and the watch memory (video IDs only, no
transcripts) is a store in your own account.

**Why was a long video charged as several transcripts?** A transcript of a 24-hour live replay is as large as dozens
of ordinary videos and takes as much to process, so a video over 10 hours counts as one transcript per 10 hours
started: 10 h 30 counts 2, 24 hours counts 3. The duration is read before the transcript is downloaded: if what is
left of your maximum charge or `maxItems` does not cover it, the video is skipped, uncharged, with a record saying
so. When YouTube gives no duration (some finished live streams and premieres), the length is read from the
captions' timings instead (up to 24 hours), the record's `warning` says so, and the same rule applies. Under 10 hours,
one transcript, whatever the length.

**Do I need a YouTube API key or any setup?** No. Leave the Connection setting on its default (automatic): it is
included in the price.

**How fast is it?** About 4 seconds for one video and 77 seconds for 62 videos at the default 512 MB (measured on 9 October
2026; three of those videos were tutorials of 12 to 16 hours). More memory reads more videos at once; the start fee is
the same up to 1 GB.

**Why is a video "missing" when I can see captions on YouTube?** Check `availableLanguages` and `missingReason`:
the video may have captions only in languages you did not list while `useAnyLanguage` is off, or it may be a live
stream still in progress. If a video shows captions on YouTube and you get `missing` with "no captions on this
video", please open an issue with the URL and the run ID: we look at every report.

**What happens when YouTube changes something?** We fix breaking changes as a priority; if a run fails, open an
issue with its run ID. A run never ends as Succeeded with empty output when the failure is on our side or
YouTube's: it ends as Failed and its status says what went wrong; if it fails before its first transcript, only the
start fee is charged. A video YouTube does
not deliver comes back as a free `error` record, with `retryable: true` when a later run may succeed.

### Only new videos: limits

- **Re-checks**: up to 50 per run (videos waiting for captions, live streams, premieres), the longest-waiting first;
  the status says how many were left for the next run. Removing a source drops its pending re-checks.
- **Runs cut short**: if a run stops before its last source (time limit, `maxItems`, maximum charge), the next run
  starts with the sources it did not reach.
- **More new videos than the window**: the source's entry in `OUTPUT.inputs` has `moreNewVideos: true`
  (`OUTPUT.watch.sourcesWithMoreNewVideos` counts such sources); each later run passes over at most 2,000 already
  delivered videos to reach the older new ones.
- **Playlists**: the first 2,000 videos are checked at each run; videos without captions do not take the places of
  the `maxVideosPerSource` new videos.
- **Errors**: private, deleted, paid, age-restricted and members-only videos and past live streams without a
  recording are reported once, then no longer checked. A video YouTube did not serve only for a while gets its
  `error` record once; later runs that meet the same error add no record (counted in `OUTPUT` as `stillFailing`).
- **Several sources**: a video delivered for one source of a watch is not delivered again for another source or as a
  link. Video links share one memory per watch: add new links from run to run, and only those are transcribed.
- **Killed runs**: after a run killed without the graceful option (or out of memory), the videos it may have charged
  are delivered again for free, possibly over several runs; some of these rows may repeat rows of the killed run,
  and the status says how many.
- **Sources taken out**: a source left out of the input is remembered for 30 days (up to about 2,500 sources in
  all), so putting it back delivers nothing twice.
- **First run without `watchName`**: a large watch may first spend one or more runs identifying its channels (status "Setting
  up this watch", nothing delivered); the next runs go on, then deliver. With a `watchName`, this is not needed.

# Actor input Schema

## `urls` (type: `array`):

One per line, mixed freely: video URLs (watch, youtu.be, Shorts, live, embed) or 11-character video IDs; channels (@handle, channel URL, UC… ID); playlists (URL or PL… ID). Channels and playlists are expanded into their videos, newest first for channels.

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

Terms searched on YouTube, one per line, as typed in YouTube's search box. Each query returns up to Max videos per source videos.

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

How many videos to take from each channel, playlist or search (newest first for channels). Single video URLs are always taken. With Only new videos: the window checked for channels, uploads playlists (UU…) and searches; any other playlist is checked on its first 2,000 videos and this caps new videos per run.

## `channelContent` (type: `string`):

Which of a channel's lists to read. A tab in the URL (@name/shorts, /streams, /videos) wins over this setting. Ignored for playlists, searches and single videos.

## `channelSort` (type: `string`):

Order of a channel's videos. Most viewed uses YouTube's own popular lists for the Videos and Shorts tabs; for Live and All uploads, newest first is used and said in OUTPUT. A ?sort=p in a tab URL means the same.

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

Order of search results. For recent videos only, combine with Published after.

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

Optional. Only videos published after this point: a date (2026-10-01), a date and time in UTC (2026-10-01T08:00:00Z) or a duration (7 days, 2 weeks, 24 hours). Older videos are skipped before their transcript is downloaded, and never charged.

## `onlyNewVideos` (type: `boolean`):

Return only videos that earlier runs did not deliver for these sources; nothing is charged twice. Videos without captions yet are checked again for 3 days. For searches, add Published after. Memory: key-value store quarra-youtube-watch in your account. Schedule the Actor.

## `watchName` (type: `string`):

Optional. Names the watch's memory. Empty: the memory belongs to these channels, playlists and searches (video links you add keep it). Set a name to share one memory across changing inputs, to keep two jobs on the same sources apart, and always for a watch made only of video links. Letters, digits, spaces, dots, dashes, underscores.

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

Language codes in order of preference (en, fr, pt-BR, es…). For each one, captions written by the creator first, then auto-generated. Empty: the language spoken in the video. Every record says which language you got (language, isAutoGenerated) and lists all tracks.

## `useAnyLanguage` (type: `boolean`):

When none of your languages exists on a video, return its transcript in the language spoken (requestedLanguageFound: false). Off: return a free missing record listing the available languages instead.

## `transcriptFormat` (type: `string`):

transcript is the whole text in one string; segments is a list of {start, end, duration, text} in seconds.

## `includeSrt` (type: `boolean`):

Add an srt field with the transcript as a ready-to-save .srt subtitle file.

## `includeVtt` (type: `boolean`):

Add a vtt field with the transcript as a ready-to-save .vtt subtitle file.

## `removeSoundTags` (type: `boolean`):

Remove non-speech tags such as \[Music], \[Applause] or (laughter) from the text and segments. Handy before feeding transcripts to an LLM.

## `includeDescription` (type: `boolean`):

Add the video's full description and its tags (keywords). Title, channel, date, duration, views, likes and chapters are always included.

## `excludeMusicVideos` (type: `boolean`):

Skip videos whose YouTube category is Music before their transcript is downloaded (never charged). YouTube's automatic captions transcribe song lyrics as speech; turn this on if you only want spoken content. Interviews or lessons filed under Music are skipped too.

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

Optional cap for the whole run, counted in charged transcripts (a video over 10 hours counts one per 10 hours started). Respected exactly. Free records (no captions, errors) do not count; a run that keeps meeting videos without a transcript stops early and says why (see Pricing in the README). Empty = no cap.

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

Connection. Keep the default (automatic). Costs are included in the price.

## Actor input object example

```json
{
  "urls": [
    "https://www.youtube.com/watch?v=8jPQjjsBbIc"
  ],
  "maxVideosPerSource": 10,
  "channelContent": "videos",
  "channelSort": "newest",
  "searchSort": "relevance",
  "onlyNewVideos": false,
  "useAnyLanguage": true,
  "transcriptFormat": "both",
  "includeSrt": false,
  "includeVtt": false,
  "removeSoundTags": false,
  "includeDescription": false,
  "excludeMusicVideos": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

Every video: transcript, segments, metadata, and free missing/error records with the reason.

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

One line per video: status, title, channel, language, words, reason.

## `summary` (type: `string`):

Per-input counts (listed, transcripts, missing, errors, already delivered), videos without captions and errors.

# 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 = {
    "urls": [
        "https://www.youtube.com/watch?v=8jPQjjsBbIc"
    ],
    "maxVideosPerSource": 10,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("quarra/youtube-transcript-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "urls": ["https://www.youtube.com/watch?v=8jPQjjsBbIc"],
    "maxVideosPerSource": 10,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("quarra/youtube-transcript-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "urls": [
    "https://www.youtube.com/watch?v=8jPQjjsBbIc"
  ],
  "maxVideosPerSource": 10,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call quarra/youtube-transcript-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,quarra/youtube-transcript-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/kVKLLbb5FCW3zXI9k/builds/jw1x3aDSLNyFQQDIG/openapi.json
