# YouTube Transcript Scraper ⚡ Bulk, No API Key (`cleanfeed/youtube-transcript-downloader`) Actor

Download YouTube transcripts, subtitles and captions in bulk. Pass video URLs or IDs; get plain text, timestamped segments, video title, channel, duration and view count as JSON or CSV. No YouTube API key, no quota, no cap. Failed videos say why and are never charged.

- **URL**: https://apify.com/cleanfeed/youtube-transcript-downloader.md
- **Developed by:** [Yaniv van der Stigchel](https://apify.com/cleanfeed) (community)
- **Categories:** AI, Agents, Videos
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 3 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $5.00 / 1,000 transcript returneds

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

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

## What's an Apify Actor?

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 — bulk, no cap, LLM-ready

Extract transcripts from thousands of YouTube videos in one run. Plain text for
embeddings, timestamped segments for search and clipping. No YouTube API key, no quota,
no per-video rate-limit dance.

### Extract YouTube transcripts in bulk

Pass a whole channel's worth of video IDs and get clean JSON back. Built for
people filling a vector store or a dataset, not for copying one transcript by
hand.

- Download YouTube transcripts to JSON or CSV
- Get YouTube subtitles with timestamps
- Extract transcripts from a list of YouTube URLs
- Scrape YouTube captions without a YouTube API key
- Bulk export YouTube video transcripts for AI training

### Why this one works

Many transcript tools broke when YouTube started requiring proof-of-origin
tokens on caption URLs — they return empty results or fail silently. This
scraper uses a fallback ladder of client identities and reports which one
served each result, so you get a transcript where other tools return nothing.

### Honest failure reporting

Every item tells you exactly what happened, and **you are only charged for
transcripts actually delivered**:

| `reason` | Meaning | Charged |
|---|---|---|
| — (`ok: true`) | Transcript returned | Yes |
| `no-captions` | Video is fine, has no captions | No |
| `unavailable` | Deleted, private, or region-locked | No |
| `blocked` | Extraction route failed | No |

Feeding in a large list of older videos? Dead entries cost you nothing, and you
can tell a caption-less video apart from a deleted one.

### Language fallback

Ask for `nl` and it prefers a human-written Dutch track, falls back to
auto-generated Dutch, then to any track that exists. Most scrapers return
nothing when the exact language is missing.

### Input

| Field | Required | Description |
|---|---|---|
| `videos` | yes | Watch URLs, youtu.be links, shorts, live, embed URLs, or bare 11-character IDs |
| `language` | no | Two-letter code. Default `en`. |
| `includeSegments` | no | Include timestamps. Default `true`. |
| `maxConcurrency` | no | Parallel requests, 1–25. Default 8. |

### Example input

```json
{
  "videos": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "https://youtu.be/jNQXAC9IVRw",
    "kJQP7kiw5Fk"
  ],
  "language": "en",
  "includeSegments": true
}
```

### Output

Every row has the same fields whether it succeeded or failed, so you can
select columns without branching. Failed rows are never charged.

| Field | Type | Description |
|---|---|---|
| `success` | boolean | True when a transcript was returned. Failed rows are never charged. |
| `videoId` | string | YouTube's 11-character video identifier. |
| `videoUrl` | string | Canonical watch URL for the video. |
| `title` | string | The video's title as published. |
| `channel` | string | Display name of the channel that published the video. |
| `channelId` | string | YouTube channel identifier, stable across renames. |
| `durationSeconds` | number | Length of the video in seconds. |
| `viewCount` | integer | Views at the time of the run. |
| `language` | string | BCP-47 code of the caption track returned. |
| `isAutoGenerated` | boolean | True when the captions were machine-generated rather than human-authored. |
| `segmentCount` | integer | Number of timed segments in the transcript. |
| `fullText` | string | The complete transcript as one plain-text string. Null on failure. |
| `segments` | array | Timestamped transcript segments. Only present when includeSegments is enabled. |
| `errorCode` | string | Machine-readable failure reason. Null on success. |
| `errorMessage` | string | Human-readable explanation of the failure. Null on success. |

#### Example — success

```json
{
  "success": true,
  "videoId": "jNQXAC9IVRw",
  "videoUrl": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
  "title": "Me at the zoo",
  "channel": "jawed",
  "channelId": "UC4QobU6STFB0P71PMvOGN5A",
  "viewCount": 358194821,
  "durationSeconds": 19,
  "language": "en",
  "isAutoGenerated": false,
  "segmentCount": 3,
  "fullText": "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.",
  "errorCode": null,
  "errorMessage": null,
  "segments": [
    {
      "start": 0.84,
      "duration": 3.2,
      "text": "All right, so here we are, in front of the elephants."
    },
    {
      "start": 4.04,
      "duration": 3.6,
      "text": "The cool thing about these guys is that they have"
    },
    {
      "start": 7.64,
      "duration": 2.9,
      "text": "really, really, really long trunks."
    }
  ]
}
```

#### Example — failure

A failure still identifies the video, so you can act on it without a second lookup.

```json
{
  "success": false,
  "videoId": "s1CFmzZzO4c",
  "videoUrl": "https://www.youtube.com/watch?v=s1CFmzZzO4c",
  "title": "Board meeting recording",
  "channel": "Example Corp",
  "channelId": "UCzzz999",
  "viewCount": 412,
  "durationSeconds": 3120,
  "language": null,
  "isAutoGenerated": null,
  "segmentCount": null,
  "fullText": null,
  "errorCode": "no-captions",
  "errorMessage": "This video has no captions published, so there is no transcript to return."
}
```

#### Error codes

- `no-captions`
- `unavailable`
- `blocked`
- `empty-transcript`
- `unparseable-input`
- `error`

### Use it for

- **YouTube transcript JSON** — structured output for pipelines, not a caption file
- **Podcast transcripts** — YouTube-hosted episodes in bulk
- **Video transcript bulk export** — thousands of videos in a single run
- **Interview transcripts** — podcast and long-form interview episodes
- **Webinar and meeting transcripts** — anything recorded and posted to YouTube
- **Earnings call transcripts** — investor calls published on a company channel
- RAG and embedding pipelines, semantic video search
- Accessibility, subtitles and content repurposing
- Competitor content analysis and training datasets

### Related actors

| If you need | Use |
|---|---|
| Every video on a channel | [YouTube Channel Transcript Scraper](https://apify.com/cleanfeed/youtube-channel-transcript-downloader) |
| Every video in a playlist | [YouTube Playlist Transcript Scraper](https://apify.com/cleanfeed/youtube-playlist-transcript-downloader) |
| A channel's Shorts | [YouTube Shorts Transcript Scraper](https://apify.com/cleanfeed/youtube-shorts-transcript-downloader) |
| Videos you have not picked yet, from a keyword | [YouTube Search to Transcripts](https://apify.com/cleanfeed/youtube-search-transcript-downloader) |
| Comments instead of the spoken text | [YouTube Comments Scraper](https://apify.com/cleanfeed/youtube-comments-downloader) |

### Pricing

Pay per transcript returned. Videos without captions, deleted videos, and failed
fetches cost nothing.

### Use it from an AI agent (MCP)

This Actor is callable as a tool through the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp), so Claude, ChatGPT, Cursor and VS Code can run it directly.

Add the server to your MCP client:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com",
      "headers": {
        "Authorization": "Bearer <YOUR_APIFY_TOKEN>"
      }
    }
  }
}
```

Then ask for what you want in plain language — for example *“get the transcript of this YouTube video”* — and the agent calls `cleanfeed/youtube-transcript-downloader` with the right input. Every output field is described in the dataset schema, so the agent knows what it is getting back before it runs anything.

### Call it from code

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")

run = client.actor("cleanfeed/youtube-transcript-downloader").call(run_input={
    "videos": ["https://www.youtube.com/watch?v=jNQXAC9IVRw"],
})

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item["success"]:
        print(item)
```

#### JavaScript

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

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

const run = await client.actor('cleanfeed/youtube-transcript-downloader').call({
    videos: ["https://www.youtube.com/watch?v=jNQXAC9IVRw"],
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.filter((i) => i.success));
```

#### cURL

```bash
curl -X POST "https://api.apify.com/v2/acts/cleanfeed~youtube-transcript-downloader/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
  -H 'Content-Type: application/json' \
  -d '{"videos": ["https://www.youtube.com/watch?v=jNQXAC9IVRw"]}'
```

### Limitations

- **A residential proxy is required.** YouTube serves `playabilityStatus: ERROR` to datacenter IP ranges while the identical request succeeds from a home connection — measured 12/12 success on residential against 0/12 on a cloud host with no proxy, and 7/12 through a datacenter proxy. The input defaults to residential; changing it will break most runs. Full method and per-environment figures are published in the [reliability benchmark](https://publicapidata.com/benchmarks/youtube-transcript-reliability/).
- **Not every video has captions.** Videos with captions disabled return `errorCode: no-captions` and are never charged. This is a property of the video, not a failure of the run.
- **Captions are read, not generated.** There is no speech-to-text fallback — if YouTube publishes no caption track, no transcript exists to return.

### FAQ

#### Do I need a YouTube API key?

No. This does not use the YouTube Data API, so there is no key, no OAuth and no daily quota. The official API also cannot return auto-generated captions at all, which is most of what exists.

#### Why does my own script work locally but fail on a server?

YouTube blocks datacenter IP ranges. The same request that succeeds from your laptop returns `playabilityStatus: ERROR` from AWS, GCP or Azure. This Actor routes through residential egress by default, which is the fix. There is a symptom-by-symptom walkthrough in [this diagnostic guide](https://publicapidata.com/guides/youtube-transcript-api-not-working/).

#### What happens to videos without captions?

They return `errorCode: no-captions` with a readable `errorMessage`, and are never charged. Failed rows carry the same fields as successful ones, so nothing downstream has to branch.

#### Can I get timestamps?

Yes — set `includeSegments: true` and each row gains a `segments` array of `{ text, start, duration }`. It is off by default because it multiplies payload size.

#### Is there speech-to-text for videos with no captions?

No. This reads the caption tracks YouTube publishes. If a video has none, there is nothing to return and you are not charged for it.

### Notes

Only publicly available caption data is collected. No login, no personal data,
no video downloads.

# Actor input Schema

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

Specific YouTube videos to transcribe: watch URLs, youtu.be links or bare 11-character video IDs. For a whole channel use cleanfeed/youtube-channel-transcript-downloader, for a playlist cleanfeed/youtube-playlist-transcript-downloader, for Shorts cleanfeed/youtube-shorts-transcript-downloader, for a keyword search cleanfeed/youtube-search-transcript-downloader.

## `language` (type: `string`):

Two-letter code (en, nl, de, es...). Falls back to auto-generated captions in that language, then to any available track.

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

Return timed segments alongside plain text. Turn off for smaller payloads when you only need the text.

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

Parallel requests. Higher is faster; lower is gentler.

## `proxy` (type: `object`):

YouTube blocks datacenter IPs. Residential proxy is required for reliable results — verified 150/150 with residential vs 7/12 with datacenter. Leave as-is unless you know what you're doing.

## Actor input object example

```json
{
  "videos": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
  ],
  "language": "en",
  "includeSegments": true,
  "maxConcurrency": 8,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

All results, one row per video. Successful rows include plain text, timestamped segments, detected language, and whether captions were auto-generated. Failed rows include a reason: no-captions, unavailable, blocked, or empty-transcript.

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

Counts of delivered transcripts and each failure type for this run.

# API

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

## JavaScript example

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

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

// Prepare Actor input
const input = {
    "videos": [
        "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

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

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

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

```

## Python example

```python
from apify_client import ApifyClient

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

# Prepare the Actor input
run_input = {
    "videos": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

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

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

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

```

## CLI example

```bash
echo '{
  "videos": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call cleanfeed/youtube-transcript-downloader --silent --output-dataset

```

## MCP server setup

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

```

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/rRpb4dNd8kvxrOmRT/builds/Ckey1hHMEjB5zhNM4/openapi.json
