# YouTube Transcript & Subtitle Scraper (`abotapi/youtube-transcript-scraper`) Actor

Extract transcripts and subtitles from YouTube videos in bulk using video, playlist, channel URLs, or keyword search. Returns timed transcript segments, plain text, SRT, and WebVTT subtitle files, with optional auto-translation to other languages.

- **URL**: https://apify.com/abotapi/youtube-transcript-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Videos, Developer tools, AI
- **Features:** Limited permissions
- **Stats:** 70 total users, 10 monthly users, 49.5% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 transcript fetcheds

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: Captions, Timestamps & Subtitles

Turn any YouTube video into readable text. Give this actor a video link, a playlist link, a channel link, or a search keyword, and it returns each video's transcript as timed segments, a single block of plain text, and ready to use SRT and WebVTT subtitle files, with optional translation into another language. Export the results to JSON, CSV or Excel, or pull them straight into your app through the API.

### Why This Scraper?

- **Four ways in.** Mix direct video links or IDs, playlist links, and channel links (`@handle`, `/channel/UC...`, `/c/...`, `/user/...`) in one run, or switch to search mode to find videos by keyword.
- **Three formats per video.** Every record carries the transcript as timed `segments`, a single plain text `transcript` field, and `srt` plus `vtt` strings you can save straight to disk.
- **Language control.** List your preferred languages in priority order, or translate the result into another language YouTube supports.
- **Handles both caption types.** Works with human written captions and auto generated ones, and falls back to whatever the video offers when none of your preferred languages are available.
- **Time windows on demand.** Trim every transcript to a specific second range, and pay only for the trimmed text.
- **Built for large pulls.** Resume an interrupted playlist or channel crawl, or schedule the actor to run on a recurring basis and get only what's new, updated, or gone since last time.
- **Pay only for transcripts you get.** Failed, captions disabled, or incremental mode suppressed videos are never billed.

### Use Cases

- **Content repurposing:** turn a video's spoken words into blog posts, show notes, or social captions without watching it end to end.
- **Research and note taking:** skim long lectures, podcasts, or interviews as searchable text instead of sitting through the video.
- **Localization and accessibility:** generate subtitle files or translated transcripts for viewers who need them.
- **SEO and content indexing:** feed video transcripts into your own search tool or content database.
- **Media monitoring:** track new uploads from a channel or keyword and pull their transcripts automatically on a schedule.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live record.

| Field | Example |
| --- | --- |
| `videoId` | `"EXAMPLE_ID1"` |
| `videoUrl` | `"https://www.youtube.com/watch?v=EXAMPLE_ID1"` |
| `videoTitle` | `"Sample Video Title"` |
| `channelName` | `"Sample Channel"` |
| `channelUrl` | `"https://www.youtube.com/@SampleChannel"` |
| `durationSeconds` | `213` |
| `source` | `"python tutorial"` (the URL or search query that produced this video) |
| `sourceType` | `"search"` (also `"video"`, `"playlist"`, `"channel"`) |
| `language` | `"English"` |
| `languageCode` | `"en"` |
| `isGenerated` | `false` (auto generated caption when `true`) |
| `isTranslated` | `false` |
| `translatedTo` | `null` |
| `charCount` | `5234` |
| `segmentCount` | `142` |
| `transcript` | `"Hello and welcome to this sample transcript..."` |
| `segments` | `[{ "text": "Hello and welcome", "start": 0.0, "duration": 1.84 }, ...]` |
| `data` | `[{ "start": 0.0, "dur": 1.84, "text": "Hello and welcome" }, ...]` (same snippets as `segments`, in the `start` / `dur` / `text` shape other transcript tools use; numeric) |
| `srt` | `"1\n00:00:00,000 --> 00:00:01,840\nHello and welcome\n\n..."` |
| `vtt` | `"WEBVTT\n\n00:00:00.000 --> 00:00:01.840\nHello and welcome\n\n..."` |
| `trimmedStart` / `trimmedDuration` | `0` / `0` (the time window applied to this video; `0` means no trim) |
| `success` | `true` |
| `error` | `null` (a short message when a transcript could not be fetched) |
| `fetchedAt` | `"2026-07-30T08:13:20Z"` (when this row was fetched, ISO 8601 UTC) |

`changeType`, `changedFields`, `firstSeenAt` and `lastSeenAt` are added only when Incremental mode is on; a normal run's rows keep the shape above without them.

### How to Use

1. Pick a **mode**: `url` (paste video, playlist, or channel links) or `search` (find videos by keyword).
2. Fill in the URLs or search terms for that mode, and set your **preferred languages**.
3. Optionally trim the time window, cap how many videos to process, or turn on translation.
4. Click **Start**, then download the dataset as JSON, CSV, or Excel, or read it through the API.

**Fetch one video's transcript:**

```json
{
  "mode": "url",
  "videoUrls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],
  "languages": ["en"]
}
```

**Search YouTube by keyword and transcribe the top results:**

```json
{
  "mode": "search",
  "searchQueries": ["langgraph tutorial", "apify actor development"],
  "maxVideosPerSource": 5,
  "languages": ["en"]
}
```

**Expand a playlist and a channel, capped per source, translated to English:**

```json
{
  "mode": "url",
  "videoUrls": [
    "https://www.youtube.com/playlist?list=PLxxxxxxxxxxxxxxxx",
    "https://www.youtube.com/@SomeChannel"
  ],
  "maxVideosPerSource": 25,
  "maxVideos": 100,
  "languages": ["en"],
  "translateToLanguage": "en"
}
```

**Trim every transcript to a specific time window:**

```json
{
  "mode": "url",
  "videoUrls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],
  "startSec": 30,
  "durationSec": 30,
  "languages": ["en"]
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/youtube-transcript-scraper").call(run_input={
    "mode": "url",
    "videoUrls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],
    "languages": ["en"],
})
for video in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(video["videoTitle"], video["charCount"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/youtube-transcript-scraper').call({
  mode: 'url',
  videoUrls: ['https://www.youtube.com/watch?v=dQw4w9WgXcQ'],
  languages: ['en'],
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted crawl. Paste its run or dataset ID and this run skips every video already collected there, so a large playlist or channel pull picks up where it left off without re fetching, or re billing, the same videos.
- **Incremental mode** (`incrementalMode`) is for scheduling this actor on the same `videoUrls`/`searchQueries` setup again and again (daily, weekly) and getting only what changed. Every video is classified `NEW`, `UPDATED`, `UNCHANGED`, `REAPPEARED`, or `EXPIRED`. By default only `NEW`, `UPDATED`, and `REAPPEARED` are returned and billed; turn on `emitUnchanged` or `emitExpired` to also get those. `stateKey` names or shares one monitoring campaign's saved state. A video is `UPDATED` when any returned field differs from the last run; the run timestamp `fetchedAt` and anything you list in `ignoreFieldsForChanges` are never compared.
- `EXPIRED` is only ever produced in `mode: search`, and only once a run has scanned every tracked query to its end. In `mode: url` the tracked set is whatever links you pasted, so a missing one simply wasn't pasted this run, not confirmed gone.

#### URL shapes

| # | Shape | Example |
| --- | --- | --- |
| 1 | Watch | `https://www.youtube.com/watch?v=dQw4w9WgXcQ` |
| 2 | youtu.be short link | `https://youtu.be/dQw4w9WgXcQ` |
| 3 | Mobile watch | `https://m.youtube.com/watch?v=dQw4w9WgXcQ` |
| 4 | Shorts | `https://www.youtube.com/shorts/jNQXAC9IVRw` |
| 5 | Live (replay) | `https://www.youtube.com/live/IoL51xLkMSY` |
| 6 | Embed | `https://www.youtube.com/embed/dQw4w9WgXcQ` |
| 7 | Watch with `list=` (treated as that one video) | `https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=PLFgquLnL59alCl_2TQvOiD5Vgm1hCaGSI` |
| 8 | Playlist | `https://www.youtube.com/playlist?list=PLFgquLnL59alCl_2TQvOiD5Vgm1hCaGSI` |
| 9 | Channel `@handle` | `https://www.youtube.com/@YouTube` |
| 10 | Channel `@handle/videos` tab | `https://www.youtube.com/@YouTube/videos` |
| 11 | Channel `/channel/UC...` | `https://www.youtube.com/channel/UCBR8-60-B28hp2BmDPdntcQ` |
| 12 | Legacy `/user/` | `https://www.youtube.com/user/YouTube` |
| 13 | Legacy `/c/` | `https://www.youtube.com/c/YouTubeCreators` |
| 14 | Search results page (expanded like a keyword search) | `https://www.youtube.com/results?search_query=python+tutorial` |

**Default sample (behavior change):** an untouched URL mode run, or an API call that omits `videoUrls`, now scrapes a sample of all 14 shapes instead of failing with an empty input. Videos are deduplicated, so the 5 single video shapes on `dQw4w9WgXcQ` count once. Billing is per transcript unit (one unit for each 4,000 characters), not per video, and long videos bill many units each (one 500,000 character tutorial alone is 125 units). To keep an untouched run cheap, while the 14 sample URLs are exactly as shipped, each playlist, channel, or search source is capped at 1 video (the log says so). Measured on a local test run through the Apify proxy: 7 videos, 73 billed units, 67 of them from the single top "python tutorial" search hit. Edit the list in any way (add, remove, or replace a URL) and the cap lifts, so `maxVideosPerSource` (default 10) applies and a search or channel source can bill hundreds of units. Set `maxVideosPerSource` / `maxVideos` to bound your own run. A URL that is not a recognised shape is skipped with a warning in the log. If an API call sends only `videoUrl` (or only `searchQueries`), the platform still fills in the 14-URL sample, so the actor drops the untouched sample: only your `videoUrl` is scraped, and when only `searchQueries` is given the run switches to keyword search mode. Your own `videoUrls`, if you send any, are never dropped.

### Send results into your apps (MCP connectors)

Optionally pipe the scraped transcripts into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape: the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human readable **summary** of each record, not the full JSON. Each item becomes one entry with a **title** and its key fields flattened to plain text. The **complete record always stays in the Apify dataset**.

1. Authorize a connector once under **Apify → Settings → Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` to the page where items should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third party credentials. Leave the field empty to skip.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `"url"` | `"url"` (read `videoUrls`) or `"search"` (read `searchQueries`). The other field is ignored. |
| `videoUrl` | string | (none) | Alias for a single URL; added to `videoUrls`. URL mode only. |
| `videoUrls` | array | sample of 14 URLs (see "URL shapes") | Used when `mode = url`. Watch, youtu.be, mobile, shorts, live, and embed URLs, playlist URLs, channel URLs (`@handle`, `@handle/videos`, `/channel/`, `/c/`, `/user/`), YouTube search results URLs, or 11 character video IDs. Playlists, channels, and search results pages are expanded to their videos. |
| `searchQueries` | array | `[]` | Used when `mode = search`. Keywords to search YouTube; top results per query, up to Max videos per source, are fetched. |
| `startSec` | integer | `0` | Trim every transcript to start at this second. `0` keeps from the beginning of each video. |
| `durationSec` | integer | `0` | Trim every transcript to this many seconds from `startSec`. `0` keeps until the end of each video. |
| `maxVideosPerSource` | integer | `10` | How many videos to take from each playlist, channel, or search query. |
| `maxVideos` | integer | `100000` | Hard cap on the total number of videos processed across all sources. Up to 100000; `0` (from older saved tasks) is read as 100000. |
| `languages` | array | `["en"]` | Preferred transcript language codes in priority order. The first available one is used; if none are available, any transcript YouTube offers is returned. |
| `translateToLanguage` | string | `""` | Optional language code to translate the transcript into using YouTube's auto translation. Empty keeps the original language. |
| `targetLanguage` | string | `""` | Alias of `translateToLanguage`; used only when that field is empty. |
| `preserveFormatting` | boolean | `false` | Keep inline formatting tags (such as italics) in the transcript text instead of stripping them. |
| `resumeFromRunId` | string | (none) | Continue one interrupted run: paste a previous run ID or dataset ID and this run skips videos already collected there. |
| `incrementalMode` | boolean | `false` | Turn on for recurring monitoring of the same setup. Classifies each video as `NEW`, `UPDATED`, `UNCHANGED`, `REAPPEARED`, or `EXPIRED`. |
| `emitUnchanged` | boolean | `false` | Incremental mode only. Also return (and bill) videos whose transcript has not changed since the last run, marked `UNCHANGED`. |
| `emitExpired` | boolean | `false` | Incremental mode only, `mode: search` only. Also return videos no longer found, marked `EXPIRED`. Never billed. |
| `ignoreFieldsForChanges` | array | `[]` | Incremental mode only. Field names (top-level, or dotted for nested, e.g. `segments.start`) that never count as a change; `fetchedAt` is always ignored. Ignored fields still appear in the output. |
| `stateKey` | string | (none) | Optional name for this monitoring campaign's saved state. Leave empty to derive one automatically from the setup. |
| `proxyConfiguration` | object | Apify Proxy, `RESIDENTIAL` | Connection settings. `RESIDENTIAL` is the default; datacenter groups also fetched transcripts in a small low-volume test (US only), so you may pick one to lower proxy cost and switch back if many videos return errors. |
| `mcpConnectors` | array | (none) | Optional: send a summary of each record to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | (none) | Notion connector only: page under which records are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. Does not affect the dataset. |

### Output Example

> Sample shape: values are illustrative placeholders, not from a live video.

```json
{
  "videoId": "EXAMPLE_ID1",
  "videoUrl": "https://www.youtube.com/watch?v=EXAMPLE_ID1",
  "videoTitle": "Sample Video Title",
  "channelName": "Sample Channel",
  "channelUrl": "https://www.youtube.com/@SampleChannel",
  "durationSeconds": 213,
  "source": "https://www.youtube.com/watch?v=EXAMPLE_ID1",
  "sourceType": "video",
  "language": "English",
  "languageCode": "en",
  "isGenerated": false,
  "isTranslated": false,
  "translatedTo": null,
  "charCount": 5234,
  "segmentCount": 142,
  "transcript": "Hello and welcome to this sample transcript.\nThis is the second line of the transcript.",
  "segments": [
    { "text": "Hello and welcome to this sample transcript.", "start": 0.0, "duration": 2.32 },
    { "text": "This is the second line of the transcript.", "start": 2.32, "duration": 2.08 }
  ],
  "srt": "1\n00:00:00,000 --> 00:00:02,320\nHello and welcome to this sample transcript.\n",
  "vtt": "WEBVTT\n\n00:00:00.000 --> 00:00:02.320\nHello and welcome to this sample transcript.\n",
  "trimmedStart": 0,
  "trimmedDuration": 0,
  "success": true,
  "error": null,
  "fetchedAt": "2026-07-30T08:13:20Z"
}
```

With `incrementalMode` on, rows also carry `changeType`, `changedFields`, `firstSeenAt`, and `lastSeenAt`.

#### Output views and run summary

The dataset has two views. **Overview** lists the video, channel, language, character count and success columns. **Changes** (Incremental mode) adds `changeType`, `changedFields`, `firstSeenAt` and `lastSeenAt`. On a failed or blocked row (`success: false`), `language`, `languageCode`, `isGenerated` and `translatedTo` are `null`, the text fields (`transcript`, `srt`, `vtt`) are empty strings, `segments` and `data` are empty lists, and `charCount` and `segmentCount` are 0; `error` explains what happened. Every run that finds at least one video also saves a run summary under the `OUTPUT` key of the key-value store (inputs, videos discovered, transcripts fetched, videos failed, billed transcript units, rows pushed, videos per source; `searchQueriesResolved` counts keyword searches in search mode only).

### Plan Requirement

This actor needs a live connection to YouTube for every transcript fetch, through an Apify proxy. The `RESIDENTIAL` group is selected by default. Datacenter groups also fetched full pages and transcripts in a small, low-volume test from US exits, so you may pick one to lower proxy cost. That test was a few requests on one day: it does not prove datacenter holds at high volume or from other countries, and YouTube is known to block many datacenter IPs. If many videos return errors, switch back to `RESIDENTIAL`.

On a free Apify plan (no residential proxy) the run logs this notice and continues on a backup connection:

> The selected connection is unavailable to this run. Apify free plans do not include residential proxies. Continuing on a backup connection; some pages may still be unavailable. For residential access, upgrade at https://apify.com/pricing or supply your own proxy URLs.

Videos served that way log "Served over the backup connection (the default connection was unavailable)." If no page could be read at all, the run ends as FAILED (never a silent success with zero rows) with one of these messages:

- **Proxy refused for this account:** "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required." If you supplied your own proxy URLs and every one of them was refused, the run ends with this same message: check that their credentials are valid.
- **Residential unavailable and backup failed:** "No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested YouTube pages."
- **Requests failed:** "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search. Please try again later."

#### Pricing

Pay per event, as registered on the Store page. The current rates for each plan tier are on the **Pricing** tab.

| Event | When it is charged |
| --- | --- |
| `transcript` | One unit for each 4,000 characters (rounded up, minimum one unit) of each successfully fetched transcript that is returned in the dataset, measured on the trimmed text |
| `search-query` | One per keyword search (search mode only) that YouTube answered, even if it returned no videos; a search whose results could not be read (connection errors) is retried on a fresh connection and is not charged if it never answers |
| `apify-actor-start` | Per run start, per GB of memory (minimum one) |

Failed, captions disabled, blocked, and Incremental-mode suppressed (`UNCHANGED`) videos are never charged.

### FAQ

#### How much does it cost?

You pay per run, plus a small fee per keyword search YouTube answers (search mode only), plus a length based unit per fetched transcript, roughly one unit for each 4,000 characters of transcript text. Failed or captions disabled videos are never billed, and if you trim a transcript with `startSec`/`durationSec` you only pay for the trimmed text. The **Pricing** tab on this actor's Store page shows the current rates.

#### Is it legal to scrape YouTube transcripts?

This actor reads publicly available captions attached to a video, the same ones YouTube's own player can display. You are responsible for how you use the results: check YouTube's Terms of Service and the copyright rules that apply to the video's spoken content in your jurisdiction before republishing or redistributing a transcript, especially at scale or for commercial use.

#### What's the difference between Resume and Incremental mode?

**Resume** (`resumeFromRunId`) continues one specific interrupted run, so a large playlist or channel pull that got cut off keeps going without re fetching (and re billing) videos it already collected. **Incremental mode** is for scheduling the same `videoUrls`/`searchQueries` setup again and again and getting only what's new or changed since the last run. They solve different problems and are not normally combined.

#### Why did I get an auto generated transcript instead of the creator's captions?

The actor prefers a human written transcript over an auto generated one whenever both exist in one of your **Preferred transcript languages**. If the creator did not upload captions in any of your preferred languages, the actor falls back to whatever the video offers, which may be auto generated.

#### Why did my run fail instead of returning an empty or partial dataset?

The run fails outright when no page could be read at all (see Plan Requirement), when a `resumeFromRunId` that cannot be resolved to a run or dataset you own, or when you turn on Incremental mode together with `resumeFromRunId` when that monitoring setup already has saved state from an earlier run. Otherwise, including empty `videoUrls`/`searchQueries`, a video with disabled captions, a private or region locked video, or a temporary block from YouTube on some of the videos, still finishes the run and adds a dataset row explaining what happened, with `success` set to `false`, so a partial run always gives you every transcript that could be read. The exception is a run in which not one video got an answer from YouTube (for example every video was temporarily blocked): that run ends FAILED with the connection message described under Plan Requirement, though the rows describing each blocked video are still in the dataset.

#### Can I get only new or changed transcripts on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then classifies every video, and only new, updated, and reappeared rows are returned and billed by default.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to send a summary of each transcript to Notion, Linear, or Airtable at the end of each run.

### 🔗 Want more video data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>📱 <a href="https://apify.com/abotapi/universal-media-extractor"><b>Universal Media Extractor</b></a><br>Extract videos, audio, and metadata from 1000+ websites including YouTube, TikTok...</td><td>📱 <a href="https://apify.com/abotapi/lemon8-media-scraper"><b>Lemon8 Media Scraper</b></a><br>Lightweight Lemon8 scraper for extracting image and video URLs from posts, with optional...</td></tr>
<tr><td>📱 <a href="https://apify.com/abotapi/viewstats-channel-analytics"><b>ViewStats Scraper</b></a><br>Scrape YouTube channel analytics from ViewStats. Look up channels by handle, URL, or...</td><td>📱 <a href="https://apify.com/abotapi/tiktok-scraper"><b>TikTok Profile, Hashtag, Search, Video &amp; Trending Scraper</b></a><br>Scrape TikTok without login. Extract profiles, bios, follower stats and videos; search by...</td></tr>
<tr><td>🎬 <a href="https://apify.com/abotapi/twitch-scraper"><b>Twitch ALL IN ONE URL</b></a><br>From $1/1K. Scrape Twitch data at scale, including channels, live streams, clips, VODs...</td><td>📱 <a href="https://apify.com/abotapi/tiktok-live-recorder"><b>Tiktok Live Recorder</b></a><br>Record TikTok live streams to MP4 with full metadata, all stream quality URLs, and...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/youtube-transcript-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

## `mode` (type: `string`):

Pick how to specify videos.

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

One per line. Watch, youtu.be, mobile (m.youtube.com), shorts, live, and embed URLs, playlist URLs, channel URLs (@handle, @handle/videos, /channel/UC..., /c/..., /user/...), YouTube search results URLs (youtube.com/results?search_query=...), or 11-character video IDs. Playlists, channels, and search results pages are expanded to their videos (up to 'Max videos per source' each). The default is a sample with one example of every supported URL shape. Billing is per 4,000-character transcript unit and long videos bill many units each, so while this exact sample is untouched each playlist, channel, or search source is capped at 1 video (a local test of this sample billed 73 units); once you edit the list, 'Max videos per source' applies in full.

## `videoUrl` (type: `string`):

Alias for a single URL; added to 'Video / playlist / channel URLs'. Used only in URL mode.

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

One per line. Top results per query (up to 'Max videos per source') are fetched.

## `startSec` (type: `integer`):

Trim each transcript to start at this second. 0 keeps from the beginning of each video.

## `durationSec` (type: `integer`):

Trim each transcript to this many seconds from Start above. 0 keeps until the end of each video.

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

How many videos to take from each playlist, channel, or search query.

## `maxVideos` (type: `integer`):

Hard cap on the total number of videos processed across all sources. Up to 100000; 0 (from older saved tasks) is read as 100000.

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

Language codes in priority order, for example \["en", "en-US", "de"]. The first available one is used; if none are available, any transcript YouTube offers is returned.

## `translateToLanguage` (type: `string`):

Optional. A language code (e.g. "en") to translate the transcript into using YouTube's auto-translation. Leave empty to keep the original.

## `targetLanguage` (type: `string`):

Alias of 'Translate transcript to'. Used only when that field is empty.

## `preserveFormatting` (type: `boolean`):

Keep inline formatting tags (e.g. italics) in the transcript text instead of stripping them.

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

The RESIDENTIAL group is the default. Free Apify plans do not include residential proxy; the run then continues on a backup connection automatically. Datacenter groups also fetched transcripts in a small low-volume test; pick one to lower proxy cost, and switch back to RESIDENTIAL if many videos return errors.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID or dataset ID to continue a large pull without re-fetching (or re-billing) videos already collected there. Use this after an interrupted run, or when continuing a large playlist/channel/search in another run. For recurring monitoring of the same videos, use Incremental mode below instead.

## `incrementalMode` (type: `boolean`):

Turn this on for daily or recurring monitoring of the same videoUrls/searchQueries. The first run returns every video as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED videos. Turn on "Emit unchanged" or "Emit expired" only when you also want those videos returned (and billed). State is kept separately for each mode/URL/query/language setup; use State key when you want to name or deliberately share a monitoring campaign. A video counts as UPDATED when any returned field differs from the last run (transcript text and segments, language, title, channel, duration, and so on); the run timestamp fetchedAt is never compared, and neither are fields you list in Ignore fields. To continue one specific interrupted run instead, use Resume from a previous run above.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return videos whose transcript has not changed since the last run, marked UNCHANGED. This returns — and bills — extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return videos that were present in a previous run but no longer appear, marked EXPIRED. Only produced in Keyword search mode, and only once a run has fully scanned every tracked search query — not when Max videos capped it, when Resume was used, or when a search may have been truncated. In URL mode this can never fire: the tracked set is whatever URLs you pasted, so a missing one just wasn't pasted this run, not confirmed gone. EXPIRED rows are synthesized from the last known data, so they are never re-billed. Leave off unless you need expiry tracking.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the mode/URLs/queries/language settings — different setups then never mix state with each other.

## `ignoreFieldsForChanges` (type: `array`):

Incremental mode only. Field names that must NOT count as a change, so a difference in them alone keeps a video UNCHANGED (and unbilled). The fetchedAt timestamp is always ignored. Use a top-level name such as videoTitle or channelName, or a dotted path for a nested field such as segments.start (lists are searched element by element). Ignored fields still appear in the output.

## `mcpConnectors` (type: `array`):

Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify → Settings → Integrations, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON — the complete record stays in the dataset. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "url",
  "videoUrls": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "https://youtu.be/dQw4w9WgXcQ",
    "https://m.youtube.com/watch?v=dQw4w9WgXcQ",
    "https://www.youtube.com/shorts/jNQXAC9IVRw",
    "https://www.youtube.com/live/IoL51xLkMSY",
    "https://www.youtube.com/embed/dQw4w9WgXcQ",
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=PLFgquLnL59alCl_2TQvOiD5Vgm1hCaGSI",
    "https://www.youtube.com/playlist?list=PLFgquLnL59alCl_2TQvOiD5Vgm1hCaGSI",
    "https://www.youtube.com/@YouTube",
    "https://www.youtube.com/@YouTube/videos",
    "https://www.youtube.com/channel/UCBR8-60-B28hp2BmDPdntcQ",
    "https://www.youtube.com/user/YouTube",
    "https://www.youtube.com/c/YouTubeCreators",
    "https://www.youtube.com/results?search_query=python+tutorial"
  ],
  "searchQueries": [
    "python tutorial"
  ],
  "startSec": 0,
  "durationSec": 0,
  "maxVideosPerSource": 10,
  "maxVideos": 100000,
  "languages": [
    "en"
  ],
  "translateToLanguage": "",
  "targetLanguage": "",
  "preserveFormatting": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

Per-video transcripts: timed segments, plain text, SRT and WebVTT.

## `changes` (type: `string`):

Incremental runs: NEW, UPDATED, REAPPEARED (and optionally UNCHANGED, EXPIRED) videos with what changed.

# 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 = {
    "mode": "url",
    "videoUrls": [
        "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
        "https://youtu.be/dQw4w9WgXcQ",
        "https://m.youtube.com/watch?v=dQw4w9WgXcQ",
        "https://www.youtube.com/shorts/jNQXAC9IVRw",
        "https://www.youtube.com/live/IoL51xLkMSY",
        "https://www.youtube.com/embed/dQw4w9WgXcQ",
        "https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=PLFgquLnL59alCl_2TQvOiD5Vgm1hCaGSI",
        "https://www.youtube.com/playlist?list=PLFgquLnL59alCl_2TQvOiD5Vgm1hCaGSI",
        "https://www.youtube.com/@YouTube",
        "https://www.youtube.com/@YouTube/videos",
        "https://www.youtube.com/channel/UCBR8-60-B28hp2BmDPdntcQ",
        "https://www.youtube.com/user/YouTube",
        "https://www.youtube.com/c/YouTubeCreators",
        "https://www.youtube.com/results?search_query=python+tutorial"
    ],
    "searchQueries": [
        "python tutorial"
    ],
    "languages": [
        "en"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/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 = {
    "mode": "url",
    "videoUrls": [
        "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
        "https://youtu.be/dQw4w9WgXcQ",
        "https://m.youtube.com/watch?v=dQw4w9WgXcQ",
        "https://www.youtube.com/shorts/jNQXAC9IVRw",
        "https://www.youtube.com/live/IoL51xLkMSY",
        "https://www.youtube.com/embed/dQw4w9WgXcQ",
        "https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=PLFgquLnL59alCl_2TQvOiD5Vgm1hCaGSI",
        "https://www.youtube.com/playlist?list=PLFgquLnL59alCl_2TQvOiD5Vgm1hCaGSI",
        "https://www.youtube.com/@YouTube",
        "https://www.youtube.com/@YouTube/videos",
        "https://www.youtube.com/channel/UCBR8-60-B28hp2BmDPdntcQ",
        "https://www.youtube.com/user/YouTube",
        "https://www.youtube.com/c/YouTubeCreators",
        "https://www.youtube.com/results?search_query=python+tutorial",
    ],
    "searchQueries": ["python tutorial"],
    "languages": ["en"],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/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 '{
  "mode": "url",
  "videoUrls": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "https://youtu.be/dQw4w9WgXcQ",
    "https://m.youtube.com/watch?v=dQw4w9WgXcQ",
    "https://www.youtube.com/shorts/jNQXAC9IVRw",
    "https://www.youtube.com/live/IoL51xLkMSY",
    "https://www.youtube.com/embed/dQw4w9WgXcQ",
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=PLFgquLnL59alCl_2TQvOiD5Vgm1hCaGSI",
    "https://www.youtube.com/playlist?list=PLFgquLnL59alCl_2TQvOiD5Vgm1hCaGSI",
    "https://www.youtube.com/@YouTube",
    "https://www.youtube.com/@YouTube/videos",
    "https://www.youtube.com/channel/UCBR8-60-B28hp2BmDPdntcQ",
    "https://www.youtube.com/user/YouTube",
    "https://www.youtube.com/c/YouTubeCreators",
    "https://www.youtube.com/results?search_query=python+tutorial"
  ],
  "searchQueries": [
    "python tutorial"
  ],
  "languages": [
    "en"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/youtube-transcript-scraper --silent --output-dataset

```

## MCP server setup

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