# YouTube Transcript Extractor: Timed Captions (`luochengfengqi/youtube-transcript-extractor`) Actor

Extracts timed captions from YouTube videos through the ANDROID player endpoint behind rotating residential proxies. Returns manual or auto-generated tracks with start times, language, full text and video details, and retries bot checks on a new IP for every video.

- **URL**: https://apify.com/luochengfengqi/youtube-transcript-extractor.md
- **Developed by:** [luochengfengqi](https://apify.com/luochengfengqi) (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

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## What's an Apify Actor?

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

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

## How to integrate an Actor?

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

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

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

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

# README

## YouTube Transcript Extractor

Reads the caption track of YouTube videos and returns it as structured data: the full text, the timed segments, the language, whether the track was written by a person or generated by speech recognition, and basic details of the video. Give it video URLs or 11-character video IDs, get one dataset record per video.

The Actor does not use a browser. For every video it calls the same player endpoint that the YouTube Android app uses, picks the caption track that matches your language preference, downloads the timed text and parses it. Requests go through Apify residential proxies, and a request that YouTube answers with a bot check is repeated on a new IP address.

### What you get

- Full transcript as one clean text string, plus an optional array of timed segments (start, duration, text).
- The track that was returned: language code, language name and kind (`manual` or `auto-generated`).
- The list of all caption tracks the video offers, so you can see which languages exist.
- Video details from the player response: title, channel name and ID, length, view count, keywords and live status.
- A status on every record, so a video without captions, a deleted video or a blocked request is a visible row and not a silent gap.
- A run summary with status counts, attempts, language and track-kind counts, in the key-value store record `SUMMARY`.

Only records with status `OK` count as transcripts. See the pricing section for what a run costs.

### How it works

1. Every input entry is parsed into a video ID. Accepted are bare IDs and URLs of the forms `watch?v=`, `youtu.be/`, `/shorts/`, `/embed/`, `/live/` and `music.youtube.com`. Playlists, channels and search URLs become `INVALID_INPUT` records. Duplicates are skipped and counted.
2. For each video the Actor sends a POST request to `https://www.youtube.com/youtubei/v1/player` as the `ANDROID` client (version 20.10.38) through a residential proxy session.
3. The response is classified. A playable video with caption tracks continues. A video that does not exist, is private, age-restricted or an unavailable live recording ends as `UNAVAILABLE`. A playable video without tracks ends as `NO_CAPTIONS`. A "Sign in to confirm you are not a bot" answer is treated as a blocked request and retried.
4. One track is chosen from the list: your languages are tried in order, an exact language code beats a regional variant, and a manual track beats an auto-generated one unless you turn that off.
5. The caption URL from the player response is downloaded through the same proxy session and parsed from the YouTube timed text format 3 (XML) into segments. HTML entities are decoded once, line breaks inside a caption are kept as spaces in the joined text, and emoji characters are removed.
6. If any step of an attempt fails for a transient reason (bot check, network error, HTTP error, empty caption body, request stalled for more than 22 seconds), the attempt is repeated on a new proxy session after a short pause, up to `maxAttempts` times. Answers that will not change on retry (unavailable video, no captions, no matching language) are returned immediately.
7. The record is written to the dataset. Under pay-per-event pricing, a record with status `OK` is charged as one `transcript-extracted` event together with the dataset write, and records with any other status are never charged.

### Quick start

1. Open the Input tab and paste video URLs or IDs into Videos. The prefilled input contains three public videos.
2. Keep the proxy configuration on Apify Proxy with the `RESIDENTIAL` group.
3. Start the Actor. A run with three videos takes a few seconds.
4. Open the Output tab. The dataset overview shows one row per video. Open `SUMMARY` in the key-value store for the totals.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `videos` | array of strings, required | three sample videos | YouTube video URLs or 11-character IDs. Duplicates are skipped. |
| `languages` | array of strings | `["en"]` | Language codes in order of preference, for example `en`, `de`, `pt-BR`. A base code such as `es` also matches regional tracks such as `es-419`. An exact code wins over a regional variant. Case-insensitive. |
| `fallbackToAnyLanguage` | boolean | `true` | If none of the preferred languages exists, return the first allowed track. With `false` the record becomes `NO_CAPTIONS` and lists the languages that exist. |
| `includeAutoGenerated` | boolean | `true` | Allow tracks generated by speech recognition. With `false`, videos that only have such tracks become `NO_CAPTIONS`. |
| `preferManualCaptions` | boolean | `true` | If a manual and an auto-generated track exist for the same language, return the manual one. |
| `includeSegments` | boolean | `true` | Add the `segments` array. With `false` only `text` and the statistics are returned, which keeps the dataset smaller. |
| `maxConcurrency` | integer, 1 to 20 | `5` | Number of videos processed at the same time. |
| `maxAttempts` | integer, 1 to 5 | `3` | Attempts per video before it is reported as `BLOCKED` or `ERROR`. Every attempt uses a new residential IP. |
| `proxyConfiguration` | object | Apify Proxy, group `RESIDENTIAL`, country `US` | Residential proxies are required. See the limitations section. |

#### Example input

```json
{
    "videos": [
        "jNQXAC9IVRw",
        "https://youtu.be/dQw4w9WgXcQ",
        "https://www.youtube.com/watch?v=UF8uR6Z6KLc"
    ],
    "languages": ["de", "en"],
    "fallbackToAnyLanguage": true,
    "includeAutoGenerated": true,
    "includeSegments": true,
    "maxConcurrency": 5,
    "maxAttempts": 3,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "US"
    }
}
```

### Output Schema

The default dataset holds one record per input entry that is a video, plus one `INVALID_INPUT` record per entry that is not. Every record has all fields listed below. Fields that do not apply are `null` (or an empty array for list fields), so the columns stay stable when you export to CSV or Excel.

#### Field Reference

| Field | Type | Description |
| --- | --- | --- |
| `input` | string | The entry exactly as it was provided. |
| `videoId` | string or null | The 11-character video ID. `null` for `INVALID_INPUT`. |
| `url` | string or null | Canonical watch URL built from the ID. |
| `status` | string | `OK`, `NO_CAPTIONS`, `UNAVAILABLE`, `BLOCKED`, `ERROR` or `INVALID_INPUT`. |
| `message` | string or null | Explanation for every status except `OK`. |
| `title` | string or null | Video title. |
| `channelId` | string or null | Channel ID. |
| `channelName` | string or null | Channel name. |
| `lengthSeconds` | integer or null | Video length in seconds. |
| `viewCount` | integer or null | View count at the time of the request. |
| `keywords` | array of strings | Keywords set by the uploader. |
| `isLive` | boolean or null | `true` for live streams and live recordings. |
| `language` | string or null | Language code of the returned track, for example `en` or `de-DE`. |
| `languageName` | string or null | Display name of the returned track, for example `English (auto-generated)`. |
| `trackKind` | string or null | `manual` or `auto-generated`. |
| `availableLanguages` | array of objects | Every caption track the video offers: `languageCode`, `name`, `trackKind`. |
| `segmentCount` | integer or null | Number of caption segments. |
| `wordCount` | integer or null | Number of whitespace-separated words in `text`. Not meaningful for languages written without spaces. |
| `transcriptDurationSeconds` | number or null | End time of the last segment in seconds. |
| `text` | string or null | The whole transcript as one string. |
| `segments` | array or null | `{ start, duration, text }` per caption segment, times in seconds with millisecond precision. `null` when `includeSegments` is `false`. |
| `attempts` | integer | Attempts used for this video. |
| `scrapedAt` | string or null | ISO 8601 timestamp (UTC) of the record. |

#### Status values

| Status | Meaning | Counts as a transcript |
| --- | --- | --- |
| `OK` | A caption track was downloaded and parsed. | Yes |
| `NO_CAPTIONS` | The video is playable but has no track that matches your settings. `message` lists the languages that exist. | No |
| `UNAVAILABLE` | The video does not exist, is private, age-restricted or an unavailable live recording. `message` carries the reason given by YouTube. | No |
| `BLOCKED` | Every attempt was answered with a bot check. Raise `maxAttempts` or run again. | No |
| `ERROR` | Every attempt failed with a network error, an HTTP error or an empty caption body. `message` carries the last error. | No |
| `INVALID_INPUT` | The entry is not a video URL or ID. | No |

#### Example record

This record was produced by a cloud run on 2026-10-21 (video "Me at the zoo").

```json
{
    "input": "jNQXAC9IVRw",
    "videoId": "jNQXAC9IVRw",
    "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
    "status": "OK",
    "message": null,
    "title": "Me at the zoo",
    "channelId": "UC4QobU6STFB0P71PMvOGN5A",
    "channelName": "jawed",
    "lengthSeconds": 19,
    "viewCount": 445555035,
    "keywords": ["me at the zoo", "jawed karim", "first youtube video"],
    "isLive": false,
    "language": "en",
    "languageName": "English",
    "trackKind": "manual",
    "availableLanguages": [
        { "languageCode": "en", "name": "English", "trackKind": "manual" },
        { "languageCode": "de", "name": "German", "trackKind": "manual" }
    ],
    "segmentCount": 6,
    "wordCount": 39,
    "transcriptDurationSeconds": 18.881,
    "text": "All right, so here we are, in front of the elephants the cool thing about these guys is that they have really... really really long trunks and that's cool (baaaaaaaaaaahhh!!) and that's pretty much all there is to say",
    "segments": [
        { "start": 1.2, "duration": 2.16, "text": "All right, so here we are, in front of the elephants" },
        { "start": 5.318, "duration": 2.656, "text": "the cool thing about these guys is that they have really..." },
        { "start": 7.974, "duration": 4.642, "text": "really really long trunks" },
        { "start": 12.616, "duration": 1.751, "text": "and that's cool" },
        { "start": 14.421, "duration": 1.312, "text": "(baaaaaaaaaaahhh!!)" },
        { "start": 16.881, "duration": 2, "text": "and that's pretty much all there is to say" }
    ],
    "attempts": 1,
    "scrapedAt": "2026-10-21T04:53:35.386Z"
}
```

#### Example of a record that is not OK

```json
{
    "input": "nfWlot6h_JM",
    "videoId": "nfWlot6h_JM",
    "status": "NO_CAPTIONS",
    "message": "No caption tracks are available for this video.",
    "language": null,
    "text": null,
    "segments": null,
    "attempts": 1
}
```

Only the fields that matter here are shown. The real record contains all fields from the reference table, with the video details filled in.

#### Run summary

The key-value store record `SUMMARY` is written at the end of every run. This is the summary of a real run with 33 videos:

```json
{
    "startedAt": "2026-10-21T04:55:44.907Z",
    "finishedAt": "2026-10-21T04:56:51.402Z",
    "recordsWritten": 33,
    "duplicatesSkipped": 0,
    "statusCounts": { "OK": 31, "NO_CAPTIONS": 2 },
    "transcriptsReturned": 31,
    "attemptsTotal": 37,
    "averageAttemptsPerTranscript": 1.13,
    "languageCounts": { "en": 30, "ko": 1 },
    "trackKindCounts": { "manual": 26, "auto-generated": 5 },
    "segmentsTotal": 2642,
    "wordsTotal": 19346,
    "chargeLimitReached": false
}
```

`attemptsTotal` counts all attempts of the run, including those of records that are not `OK`. `averageAttemptsPerTranscript` averages the attempts of `OK` records only.

### Pricing

The Actor has no per-result price at the moment: you pay only the Apify platform usage of your runs, listed below. The Actor is prepared for the pay-per-event model with one event, `transcript-extracted`, for every record with status `OK`. If that pricing is switched on, records with any other status stay free and the price is shown on the Actor page before you run it.

Under pay-per-event pricing, if you set a maximum charge for the run, the Actor stops starting new videos once the limit is reached and sets `chargeLimitReached` in the summary. Videos that were already in flight may not be written to the dataset, because the platform stops accepting data over the limit.

#### Platform usage per transcript

Platform usage (residential proxy traffic, compute, storage) is billed to the account that runs the Actor while the Actor has no per-result price; under pay-per-event pricing the Actor owner would cover it instead. Measured in cloud runs on 2026-10-21 with 512 MB of memory on a free-plan account, so unit prices on your plan may differ:

| Run | Videos | OK | Platform usage | Per OK transcript |
| --- | --- | --- | --- | --- |
| Default input, three videos | 3 | 3 | USD 0.00127 | USD 0.00042 |
| Mixed set with a 4.4 hour and a 3.4 hour course | 9 | 7 | USD 0.00478 | USD 0.00068 |
| 33 music videos and talks, concurrency 10 | 33 | 31 | USD 0.01486 | USD 0.00048 |

Residential traffic was about 55 KB per transcript and 92 percent of the cost in the 33-video run. Long videos are not expensive: the whole second run, including the two multi-hour courses (about 2,900 segments each), used about 0.55 MB of residential traffic, because the caption XML is transferred compressed.

The 33-video run includes one proxy connection that stalled for about a minute, which raised its compute share. The Actor now cuts every request after 22 seconds and retries it on a new IP.

### Reliability

Measured on 2026-10-21 from the Apify US residential pool, 49 valid video requests in 6 cloud runs:

- 43 returned a transcript, 4 returned `NO_CAPTIONS` (2 videos without captions, 2 caused by the settings `includeAutoGenerated: false` and `fallbackToAnyLanguage: false` that were used in tests), 2 returned `UNAVAILABLE` (a non-existent ID and a live stream).
- 42 of the 49 videos reached their final answer on the first attempt. The other 7 failed their first attempt and reached a final answer on the second attempt. For 5 of these the logged reason was a bot check (2), a proxy `UPSTREAM504` (2) or an `ECONNRESET` (1); the other 2 happened before retry logging was added. No video needed a third attempt, and no video ended as `BLOCKED` or `ERROR`.
- Without a residential proxy the same endpoint answered most requests with the bot check in earlier tests (1 of 5 videos passed from a cloud datacenter, 1 of 3 from a local connection), so the proxy is part of the design and not an option.

This is a small sample from one day. YouTube changes its anti-bot rules without notice, so check the `statusCounts` in the summary of your own runs.

### Usage via API

Replace `YOUR_USERNAME` with the account that owns the Actor and `YOUR_APIFY_TOKEN` with an API token.

#### cURL

```bash
curl -X POST "https://api.apify.com/v2/acts/YOUR_USERNAME~youtube-transcript-extractor/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "videos": ["jNQXAC9IVRw", "https://youtu.be/dQw4w9WgXcQ"],
    "languages": ["en"],
    "includeSegments": false
  }'
```

The synchronous endpoint waits for the run and has a time limit of 300 seconds. For large batches start an asynchronous run with `POST /v2/acts/YOUR_USERNAME~youtube-transcript-extractor/runs` and read the dataset after the run has finished.

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")

run = client.actor("YOUR_USERNAME/youtube-transcript-extractor").call(
    run_input={
        "videos": ["jNQXAC9IVRw", "https://youtu.be/dQw4w9WgXcQ"],
        "languages": ["en"],
        "includeSegments": True,
    }
)

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item["status"] == "OK":
        print(item["videoId"], item["language"], item["trackKind"], item["wordCount"])
    else:
        print(item["input"], item["status"], item["message"])
```

#### Node.js

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

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });

const run = await client.actor('YOUR_USERNAME/youtube-transcript-extractor').call({
    videos: ['jNQXAC9IVRw', 'https://youtu.be/dQw4w9WgXcQ'],
    languages: ['en'],
    includeSegments: true,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const item of items) {
    if (item.status === 'OK') console.log(item.videoId, item.language, item.trackKind, item.wordCount);
    else console.log(item.input, item.status, item.message);
}
```

### Tips

- To get subtitles for a specific language, put it first in `languages` and set `fallbackToAnyLanguage` to `false`. Videos without that language then come back as `NO_CAPTIONS` with the list of available languages in `message`.
- If you only need the text, set `includeSegments` to `false`. The dataset is much smaller and the cost is the same.
- To build a clean training or search corpus, set `includeAutoGenerated` to `false` and filter on `trackKind`.
- For lists of hundreds of videos raise `maxConcurrency` to 10. Residential traffic per transcript does not depend on concurrency.
- Retry only the failed videos: filter the dataset for `BLOCKED` and `ERROR` records and run the Actor again with their `input` values.

### Limitations and Notes

- The Actor depends on an undocumented YouTube endpoint and on the `ANDROID` client identity. YouTube can change or remove it without notice. The client version is fixed in `src/player.js`.
- Residential proxies are required. In tests without one, most videos were answered with the bot check, while the residential pool returned a final answer for every video within two attempts. Running with `useApifyProxy: false` is possible but most videos come back as `BLOCKED`.
- Only public videos that offer a caption track are supported. Private, age-restricted and removed videos come back as `UNAVAILABLE`. A 24/7 live stream was also returned as `UNAVAILABLE` in a test; members-only videos were not tested.
- Translated captions (automatic translation into another language) are not supported. Only the tracks listed in `availableLanguages` can be returned.
- Auto-generated captions can contain overlapping segments, because YouTube shows two lines at once. `text` is built from the segments in order, so overlapping lines are not merged.
- Emoji characters are removed from titles and caption text. Text symbols such as music note signs, the copyright sign and the trademark sign are kept.
- `wordCount` splits on whitespace, so it is not a reliable length measure for Chinese, Japanese and Thai.
- `viewCount` and the other video details are taken from the player response at the time of the request. The player response of this client does not contain the publication date, so the Actor does not return one.
- Playlists, channels, search results and comments are out of scope. Pass individual video URLs or IDs.
- Run locally with `apify run` only for testing: accounts without the external proxy access feature cannot use Apify Proxy outside the platform, and a direct connection from a datacenter is usually blocked.
- You are responsible for using the data in line with the YouTube Terms of Service and the copyright of the video owners. This Actor is not affiliated with or endorsed by YouTube or Google.

### Local development

```bash
npm install
npm test
apify run
```

`npm test` runs the unit tests of the input parser, caption parser, player response classifier, retry logic and the consistency checks between the code defaults and the files in `.actor`. The tests use real responses that were captured from YouTube as fixtures, and a fake network boundary for the retry logic.

# Actor input Schema

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

YouTube video URLs or 11-character video IDs, one per entry. Accepted URL forms: watch?v=, youtu.be, /shorts/, /embed/, /live/ and music.youtube.com. Playlists, channels and search URLs are not supported and produce an INVALID_INPUT record. Duplicate videos are skipped.

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

Language codes in order of preference, for example 'en', 'de' or 'pt-BR'. A base code such as 'es' also matches regional tracks such as 'es-419', and an exact code wins over a regional variant. Matching is case-insensitive.

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

When none of the preferred languages exists, return the first available track instead of a NO_CAPTIONS record. The language field of the record shows which track was returned.

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

Allow tracks that YouTube generated by speech recognition. When disabled, videos that only have auto-generated captions return a NO_CAPTIONS record.

## `preferManualCaptions` (type: `boolean`):

When both a manual and an auto-generated track exist for the same language, return the manual one. Disable it to take the auto-generated track in that case.

## `includeSegments` (type: `boolean`):

Add the 'segments' array with start time, duration and text of every caption line. When disabled, only the joined 'text' and the statistics are returned, which keeps the dataset smaller.

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

Maximum number of videos processed at the same time (1 to 20). Every video uses its own residential proxy session.

## `maxAttempts` (type: `integer`):

How many times a video is tried before it is reported as BLOCKED or ERROR (1 to 5). Every attempt uses a new residential IP. Only successful transcripts are charged, so extra attempts cost proxy traffic but no events.

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

Residential proxies are required. In testing, direct and datacenter connections were answered with a bot check for most videos, while residential IPs passed within three attempts. Keep the RESIDENTIAL group selected.

## Actor input object example

```json
{
  "videos": [
    "dQw4w9WgXcQ",
    "UF8uR6Z6KLc",
    "iG9CE55wbtY"
  ],
  "languages": [
    "en"
  ],
  "fallbackToAnyLanguage": true,
  "includeAutoGenerated": true,
  "preferManualCaptions": true,
  "includeSegments": true,
  "maxConcurrency": 5,
  "maxAttempts": 3,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}
```

# Actor output Schema

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

No description

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

No description

# API

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

## JavaScript example

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

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

// Prepare Actor input
const input = {
    "videos": [
        "dQw4w9WgXcQ",
        "UF8uR6Z6KLc",
        "iG9CE55wbtY"
    ],
    "languages": [
        "en"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "US"
    }
};

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

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

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

```

## Python example

```python
from apify_client import ApifyClient

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

# Prepare the Actor input
run_input = {
    "videos": [
        "dQw4w9WgXcQ",
        "UF8uR6Z6KLc",
        "iG9CE55wbtY",
    ],
    "languages": ["en"],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "US",
    },
}

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

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

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

```

## CLI example

```bash
echo '{
  "videos": [
    "dQw4w9WgXcQ",
    "UF8uR6Z6KLc",
    "iG9CE55wbtY"
  ],
  "languages": [
    "en"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}' |
apify call luochengfengqi/youtube-transcript-extractor --silent --output-dataset

```

## MCP server setup

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

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/QgojNbKJF7nqMn19u/builds/qHl9oPEf2oZ3czPef/openapi.json
