# YouTube Transcript Scraper - Captions, SRT, VTT & RAG (`sebastian-actors/youtube-transcript-scraper`) Actor

Unofficial and not affiliated with YouTube or Google. Extract clean, timestamped transcripts from public videos and Shorts with language selection, translation, subtitles, Markdown, and RAG-ready chunks.

- **URL**: https://apify.com/sebastian-actors/youtube-transcript-scraper.md
- **Developed by:** [Sebastián S](https://apify.com/sebastian-actors) (community)
- **Categories:** Videos, Social media, 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 transcript extracteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

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

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## YouTube Transcript Scraper

> **Unofficial integration.** This Actor is not affiliated with, endorsed by, or sponsored by YouTube or Google.

`youtube-transcript-scraper` is an HTTP-only Apify Actor for extracting captions from individual public YouTube videos. It validates every input before networking, selects caption tracks deterministically, and emits clean text, timestamped segments, SRT, WebVTT, Markdown, or segment-aware RAG chunks.

The Actor does not use a browser, download video or audio, sign in, transcribe audio, crawl playlists or channels, scrape comments, or generate AI summaries.

### Input

```json
{
    "videoUrls": ["aircAruvnKk", "https://youtu.be/aircAruvnKk"],
    "preferredLanguages": ["en-US", "en", "es"],
    "captionPreference": "manual-first",
    "fallbackToAnyLanguage": true,
    "translateTo": "es",
    "outputFormats": ["text", "segments", "srt", "vtt", "markdown", "chunks"],
    "cleaning": {
        "decodeHtmlEntities": true,
        "normalizeWhitespace": true,
        "normalizeUnicode": true,
        "deduplicateAdjacentSegments": true,
        "removeNonSpeechCues": false,
        "paragraphGapSeconds": 2.5
    },
    "chunking": {
        "maxCharacters": 2000,
        "overlapCharacters": 200,
        "includeTimestamps": true
    },
    "includeMetadata": true,
    "maxConcurrency": 5,
    "maxRetriesPerVideo": 3,
    "requestTimeoutSecs": 30,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": ["RESIDENTIAL"]
    },
    "debug": false
}
```

| Field                   | Default                 | Limits and behavior                                                                           |
| ----------------------- | ----------------------- | --------------------------------------------------------------------------------------------- |
| `videoUrls`             | Required                | 1 to 1,000 individual URLs or raw 11-character IDs.                                           |
| `preferredLanguages`    | `["en"]`                | Up to 20 ordered BCP 47 codes; exact matches precede base-language matches.                   |
| `captionPreference`     | `manual-first`          | `manual-first`, `auto-first`, `manual-only`, or `auto-only`.                                  |
| `fallbackToAnyLanguage` | `true`                  | Allows a deterministic fallback when preferences do not match.                                |
| `translateTo`           | Unset                   | Uses a native target track first, then an advertised YouTube translation.                     |
| `outputFormats`         | `["text", "segments"]`  | Any unique non-empty selection of `text`, `segments`, `srt`, `vtt`, `markdown`, and `chunks`. |
| `cleaning`              | Shown above             | Controls entity, Unicode, whitespace, non-speech, deduplication, and paragraph behavior.      |
| `chunking`              | Shown above             | 250 to 20,000 code points per chunk; overlap must be smaller than the maximum.                |
| `includeMetadata`       | `true`                  | When false, every metadata field is `null`.                                                   |
| `maxConcurrency`        | `5`                     | 1 to 20 videos.                                                                               |
| `maxRetriesPerVideo`    | `3`                     | 0 to 5 complete session retries.                                                              |
| `requestTimeoutSecs`    | `30`                    | 10 to 120 seconds per upstream request.                                                       |
| `proxyConfiguration`    | Residential Apify Proxy | Standard Apify proxy configuration; residential IPs are the reliable default for YouTube.     |
| `debug`                 | `false`                 | Emits sanitized phase diagnostics only.                                                       |

Supported URLs include `youtube.com/watch`, `youtu.be`, `/shorts/`, `/embed/`, `/live/`, `music.youtube.com/watch`, and `youtube-nocookie.com/embed`. Watch URLs may contain playlist or tracking parameters; all parameters except the validated video ID are discarded. Credentials, custom ports, foreign hosts, unsupported protocols, and malformed IDs are rejected before any request. Known channel, playlist, profile, and search URLs produce a per-item `UNSUPPORTED_INPUT_TYPE` row.

Valid inputs are deduplicated by video ID. The first input and `inputIndex` are retained. Invalid entries are not deduplicated. Dataset order is completion order; use `inputIndex` to recover source order.

### Selection

The Actor normalizes the player caption tracks once. It treats `kind=asr` as automatic and every other track as manual. Preferred languages are ranked by ordered exact match, then ordered base-language match. Caption preference, default-track status, and original track order break ties deterministically.

When `translateTo` is set, an existing exact or base-matching native target track wins. Otherwise, the source track is selected normally and translation proceeds only when YouTube advertises the requested target language.

### Output

One default dataset item is written per unique video and per invalid input. Success and error rows share schema version `1.0`. Every row includes:

- Input identity, status, validated ID, and canonical URL.
- Nullable title, channel ID/name, duration, publication date, thumbnail, `isShort`, and `isLive` metadata.
- Selected/output/source language, caption type, generated/translated flags, and available tracks.
- Requested artifacts plus word, character, and segment counts.
- Provider, extraction duration, external storage references, typed error fields, and fetch time.

Unrequested transcript artifact fields are `null`. `isShort` is `true` only when the retained input is an explicit `/shorts/{id}` URL; otherwise it is `null`. Metadata is best-effort and nullable.

```json
{
    "schemaVersion": "1.0",
    "inputIndex": 0,
    "input": "aircAruvnKk",
    "status": "success",
    "videoId": "aircAruvnKk",
    "canonicalUrl": "https://www.youtube.com/watch?v=aircAruvnKk",
    "title": "Example video",
    "channelId": "UC000000000000000000000",
    "channelName": "Example Channel",
    "durationSeconds": 212,
    "publishedAt": "2025-04-10T00:00:00.000Z",
    "thumbnailUrl": "https://i.ytimg.com/vi/aircAruvnKk/hqdefault.jpg",
    "isShort": null,
    "isLive": false,
    "languageCode": "en",
    "languageName": "English",
    "sourceLanguageCode": "en",
    "captionType": "manual",
    "isGenerated": false,
    "isTranslated": false,
    "availableTracks": [],
    "transcriptText": "Example transcript text",
    "segments": [],
    "srt": null,
    "vtt": null,
    "markdown": null,
    "chunks": null,
    "wordCount": 3,
    "characterCount": 23,
    "segmentCount": 1,
    "provider": "youtubei-transcript-endpoint",
    "extractionMs": 740,
    "contentStoredExternally": false,
    "keyValueStoreId": null,
    "artifactKeys": null,
    "errorCode": null,
    "errorMessage": null,
    "retryable": null,
    "fetchedAt": "2026-07-31T22:10:00.000Z"
}
```

Text is generated only from normalized source cues. The Actor decodes entities, removes supported caption tags, normalizes whitespace and Unicode when enabled, and never invents punctuation. Adjacent cues are deduplicated only when their text is identical and their shorter time interval overlaps by at least 80%. Chunk sizes count Unicode code points; overlap retains whole segments. Only a single oversized segment is split at word boundaries.

### Large Results

Every dataset row is measured as UTF-8 JSON before it is written. If a successful item exceeds 8 MiB, every non-null transcript artifact is stored under stable keys in the default key-value store. Inline artifact fields become `null`, `contentStoredExternally` becomes `true`, and `keyValueStoreId` plus `artifactKeys` identify the complete files. Content is never silently truncated.

### Errors And Retries

The stable error codes are:

`INVALID_VIDEO_INPUT`, `UNSUPPORTED_INPUT_TYPE`, `VIDEO_UNAVAILABLE`, `PRIVATE_VIDEO`, `MEMBERS_ONLY`, `AGE_RESTRICTED`, `LIVE_NOT_ENDED`, `TRANSCRIPTS_DISABLED`, `NO_CAPTIONS`, `NO_MATCHING_LANGUAGE`, `TRANSLATION_UNAVAILABLE`, `PO_TOKEN_REQUIRED`, `RATE_LIMITED`, `IP_BLOCKED`, `REQUEST_TIMEOUT`, `NETWORK_ERROR`, `YOUTUBE_RESPONSE_CHANGED`, `BUDGET_EXHAUSTED`, and `INTERNAL_ERROR`.

Only temporary network, proxy, rate-limit, timeout, HTTP 5xx, and unexpected empty-response failures retry. Backoff uses capped exponential jitter and honors `Retry-After`. One attempt keeps its proxy identity, cookie jar, and anonymous YouTube session. The complete session rotates only for the next attempt.

YouTube increasingly marks some timedtext tracks with `exp=xpe`, requiring a session-bound Proof of Origin token. HTTP-only anonymous extraction cannot solve this reliably. These tracks, and repeated empty HTTP 200 timedtext responses, return `PO_TOKEN_REQUIRED` rather than a generic internal error. See the observed [PO-token behavior](https://github.com/jdepoix/youtube-transcript-api/issues/592).

### Proxies And Privacy

Residential Apify Proxy is enabled by default because YouTube commonly rejects datacenter IPs. You can change the group or provide your own proxy URLs through the standard Console editor. A distinct session ID is generated per video attempt. Redirects are allowed only to the YouTube hosts required by youtubei.js, and timedtext URLs must remain HTTPS `www.youtube.com/api/timedtext`.

The Actor never logs proxy URLs, cookies, tokens, transcript contents, or raw YouTube response bodies. Debug logs contain only sanitized video IDs, phases, error codes, and retry numbers.

### Pay Per Event

Successful rows are written with the custom `transcript-extracted` event. Error rows are written without a custom event. In the Apify Console pricing configuration:

1. Keep the synthetic `apify-actor-start` event enabled.
2. Define the custom `transcript-extracted` event and its price.
3. Disable the synthetic `apify-default-dataset-item` event so error rows are not charged.

The Actor checks remaining event capacity before a success commit and serializes commits across concurrent workers. A successfully charged final item is preserved when the limit becomes exhausted. Remaining work receives free `BUDGET_EXHAUSTED` rows. See Apify's [pay-per-event documentation](https://docs.apify.com/actors/publishing/monetize/pay-per-event).

### Resume Behavior

`STATE` in the default key-value store contains configuration-sensitive result hashes and counters. State is updated only after a dataset write and explicitly persisted after every committed item. Restarted runs skip completed keys. Changing a transcript-affecting option creates a different result key. `OUTPUT` contains the aggregate summary, timing, provider/error counts, charge count, externalization count, and budget state.

### Development

Node.js 22 is required. Dependencies are pinned exactly and `package-lock.json` is committed.

```bash
npm ci
npm run typecheck
npm run lint
npm test
npm run build
```

Default tests use committed sanitized fixtures and make no YouTube requests. Run the optional live smoke test explicitly:

```bash
RUN_LIVE_TESTS=true npm test -- tests/live
```

Local Actor execution uses `storage/key_value_stores/default/INPUT.json`:

```bash
apify run --purge
```

### Limitations And Legal Use

Only public individual videos are supported. Private, members-only, authenticated, and active live content is intentionally unsupported. Captions can be absent, disabled, geo-restricted, rate-limited, IP-blocked, changed by YouTube, or protected by a proof token. No bypass of access controls is attempted.

Use this Actor only where you have the right to retrieve and process the material. Follow YouTube's terms, applicable copyright and privacy law, and the rules of the jurisdiction in which you operate. You are responsible for your inputs, retention, redistribution, and downstream use.

# Actor input Schema

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

Individual YouTube video URLs or raw 11-character video IDs. Channels, playlists, profiles, and search pages are returned as per-item errors.

## `preferredLanguages` (type: `array`):

Ordered BCP 47 language preferences. Exact regional matches beat base-language matches.

## `captionPreference` (type: `string`):

Choose whether human-created or automatically generated captions are preferred or required.

## `fallbackToAnyLanguage` (type: `boolean`):

Select another available language when no preferred language matches.

## `translateTo` (type: `string`):

Optional YouTube-supported target language code. Existing native target-language captions are preferred.

## `outputFormats` (type: `array`):

Transcript artifacts to include in each successful result.

## `cleaning` (type: `object`):

Configure deterministic text normalization. No punctuation is invented.

## `chunking` (type: `object`):

Configure segment-aware chunks generated when the chunks format is selected.

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

Include available title, channel, duration, publication, thumbnail, Shorts, and live metadata.

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

Number of videos processed concurrently.

## `maxRetriesPerVideo` (type: `integer`):

Number of complete session retries after the first attempt.

## `requestTimeoutSecs` (type: `integer`):

Timeout applied to each upstream HTTP request.

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

Use Apify Proxy or your own proxy URLs for YouTube requests.

## `debug` (type: `boolean`):

Log sanitized phase diagnostics without transcripts, cookies, tokens, response bodies, or proxy URLs.

## Actor input object example

```json
{
  "videoUrls": [
    "https://www.youtube.com/watch?v=aircAruvnKk"
  ],
  "preferredLanguages": [
    "en"
  ],
  "captionPreference": "manual-first",
  "fallbackToAnyLanguage": true,
  "outputFormats": [
    "text",
    "segments"
  ],
  "cleaning": {
    "decodeHtmlEntities": true,
    "normalizeWhitespace": true,
    "normalizeUnicode": true,
    "deduplicateAdjacentSegments": true,
    "removeNonSpeechCues": false,
    "paragraphGapSeconds": 2.5
  },
  "chunking": {
    "maxCharacters": 2000,
    "overlapCharacters": 200,
    "includeTimestamps": true
  },
  "includeMetadata": true,
  "maxConcurrency": 5,
  "maxRetriesPerVideo": 3,
  "requestTimeoutSecs": 30,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "debug": false
}
```

# Actor output Schema

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

One success or typed error record per unique video, plus per-item invalid-input errors.

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

Aggregate run counters stored under OUTPUT.

## `artifacts` (type: `string`):

Transcript files externalized when a dataset item would exceed 8 MiB.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

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

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "videoUrls": [
        "https://www.youtube.com/watch?v=aircAruvnKk"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("sebastian-actors/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 = { "videoUrls": ["https://www.youtube.com/watch?v=aircAruvnKk"] }

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

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

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

```

## CLI example

```bash
echo '{
  "videoUrls": [
    "https://www.youtube.com/watch?v=aircAruvnKk"
  ]
}' |
apify call sebastian-actors/youtube-transcript-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=sebastian-actors/youtube-transcript-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

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