# YouTube Channel & Playlist Transcripts: Bulk & Search (`altronite/youtube-bulk-transcripts`) Actor

Bulk YouTube transcripts from channels, playlists and keyword searches, with date, length and Shorts filters, a new-only mode and markdown for RAG.

- **URL**: https://apify.com/altronite/youtube-bulk-transcripts.md
- **Developed by:** [Altronite](https://apify.com/altronite) (community)
- **Categories:** Videos, AI, Social media
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 transcripts

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

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 Channel & Playlist Transcripts: Bulk & Search

Get the transcripts of a whole YouTube channel, a playlist or a keyword search in one run. Paste channel links or @handles, playlist links, search keywords or single video links; the actor lists the videos, applies your filters (date, length, Shorts), reads each video's own captions and gives you one row per video, plus one markdown file per source that is ready for an LLM or a RAG index.

**You pay only for transcripts you receive.** Videos without captions, private or removed videos, videos YouTube keeps blocking, filtered videos and videos already delivered in an earlier run are free.

### Who it's for

- **LLM and RAG builders** who want a channel's or a topic's spoken content as clean text, in one corpus file per source.
- **Researchers and analysts** who track what is said across a channel, a playlist or a search over time.
- **Monitoring and automation** (schedules, n8n, Make, Zapier): with `onlyNewSinceLastRun`, a daily run returns only the new uploads.
- **Content teams** who repurpose talks, podcasts and tutorials into notes, articles or study material.

### What you get

One row per video in the dataset:

| Field | Example |
|---|---|
| `status` | `ok` or `error` |
| `videoId`, `url` | `jNQXAC9IVRw`, `https://www.youtube.com/watch?v=jNQXAC9IVRw` |
| `title`, `channel`, `channelUrl`, `thumbnailUrl` | on every `ok` row |
| `channelId` | from channel, playlist and search listings |
| `publishedAt`, `publishedText`, `publishedAtIsEstimate` | `2005-04-24T03:31:52+00:00`; exact for a channel's newest uploads, else estimated from text like `3 weeks ago`. Channel, playlist and search listings only |
| `durationSec`, `viewCount`, `isShort` | from channel, playlist and search listings |
| `source`, `sourceType`, `position` | which channel, playlist, search or `videos` input listed the video, and its rank there |
| `language`, `languageCode`, `isAutoGenerated` | `English`, `en`, `false` |
| `isTranslated`, `languageFallback`, `availableLanguages` | how the caption track was chosen |
| `text` | the whole transcript as one plain string |
| `segments` / `srt` / `vtt` | the timed form, if you ask for it |
| `segmentCount`, `wordCount`, `captionsEndSec` | size of the transcript |
| `scrapedAt` | when the row was made, UTC (`2026-10-23T22:35:42+00:00`) |
| `reason`, `error` | on error rows only (see Troubleshooting) |

Rows that come from the `videos` input (links you paste) have no listing behind them, so they carry `title`, `channel`, `channelUrl` and `thumbnailUrl` but not `channelId`, the `published*` fields, `durationSec`, `viewCount` or `isShort`. With no date or length known, the `publishedAfter`, duration and Shorts filters let those videos through.

A real row (`outputFormat: "text"`, text shortened):

```json
{
  "status": "ok",
  "videoId": "jNQXAC9IVRw",
  "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
  "title": "Me at the zoo",
  "channel": "jawed",
  "channelId": "UC4QobU6STFB0P71PMvOGN5A",
  "channelUrl": "https://www.youtube.com/channel/UC4QobU6STFB0P71PMvOGN5A",
  "publishedAt": "2005-04-24T03:31:52+00:00",
  "durationSec": 19,
  "source": "https://www.youtube.com/@jawed",
  "sourceType": "channel",
  "position": 1,
  "publishedText": "21 years ago",
  "publishedAtIsEstimate": false,
  "isShort": false,
  "language": "English",
  "languageCode": "en",
  "isAutoGenerated": false,
  "availableLanguages": ["de", "en"],
  "segmentCount": 6,
  "wordCount": 39,
  "text": "All right, so here we are, in front of the elephants ..."
}
```

In the run's key-value store:

- **One markdown file per source**, for example `channel-jawed.md`, `playlist-plzhqobowtqdpd3mizzm2xvfitgf8he-ab.md`, `search-python-asyncio-tutorial.md` and `videos.md`. Each has a header, then every delivered transcript under its title with URL, channel, date, length and language. Turn it off with `markdownFiles: false`.
- **`SUMMARY`**: per source, how many videos were listed, delivered and skipped (and why), and the markdown file's key.

### Input

```json
{
  "channels": ["https://www.youtube.com/@3blue1brown", "@Fireship"],
  "playlists": ["https://www.youtube.com/playlist?list=PLZHQObOWTQDPD3MizzM2xVFitgF8hE_ab"],
  "searchQueries": ["python asyncio tutorial"],
  "videos": ["https://www.youtube.com/watch?v=jNQXAC9IVRw"],
  "maxVideosPerSource": 20,
  "publishedAfter": "30 days",
  "skipShorts": true,
  "outputFormat": "text"
}
```

| Field | Default | What it does |
|---|---|---|
| `channels` | none | Channel URLs, @handles or channel ids (UC...). Uploads are read newest first. |
| `playlists` | none | Playlist URLs or ids. Mixes (`list=RD...`) and private lists (Watch later, Liked) can't be read. |
| `searchQueries` | none | Keywords searched on YouTube, videos only. Each query is one source. |
| `videos` | none | Single video links or ids. Filters don't apply to these. |
| `maxVideosPerSource` | 50 | Videos taken from each channel, playlist or search after filters, 1 to 5000. |
| `searchSortBy` | `relevance` | `relevance` or `date` (YouTube's upload date order). |
| `publishedAfter` | none | A date like `2026-01-01` or a period like `30 days`. |
| `minDurationSec`, `maxDurationSec` | none | Keep only videos within this length. |
| `skipShorts` | `true` | Leave out YouTube Shorts. |
| `onlyNewSinceLastRun` | `false` | Skip videos already delivered for the same source in an earlier run. |
| `languages` | `["en"]` | Caption languages in order of preference, like `en`, `de`, `pt-BR`. |
| `allowAutoGenerated` | `true` | Use YouTube's automatic captions when no human-made track exists. |
| `translateTo` | none | A language code; uses YouTube's own caption translation when allowed. |
| `outputFormat` | `text` | `text`, `segments`, `srt`, `vtt`, or `metadata` (video details only, no transcripts). |
| `markdownFiles` | `true` | One markdown file per source in the key-value store. |
| `maxConcurrency` | 5 | Videos fetched at once, 1 to 10. |
| `proxyConfiguration` | Apify Proxy on | YouTube limits cloud servers; blocked videos are retried on a fresh IP. |

Run it with an empty input and it fetches the one video of a small public channel, so you can see the output first.

More examples:

The newest 10 long videos of a channel, as a weekly schedule that returns only new uploads:

```json
{ "channels": ["@Fireship"], "maxVideosPerSource": 10, "minDurationSec": 120, "onlyNewSinceLastRun": true }
```

Keyword search straight to transcripts, last 90 days, newest first:

```json
{ "searchQueries": ["kubernetes tutorial"], "searchSortBy": "date", "publishedAfter": "90 days", "maxVideosPerSource": 25 }
```

A whole playlist as SRT subtitles, Spanish first:

```json
{ "playlists": ["PLZHQObOWTQDPD3MizzM2xVFitgF8hE_ab"], "languages": ["es", "en"], "outputFormat": "srt" }
```

Just the video list of a channel (title, date, length, views), no transcripts:

```json
{ "channels": ["@3blue1brown"], "outputFormat": "metadata", "maxVideosPerSource": 500 }
```

### Use it from code or an AI agent

Run it with the Apify API, the Apify Python or JavaScript clients, or as a tool through the Apify MCP server, with the same input JSON. Rows are in the default dataset; the markdown files and `SUMMARY` are in the default key-value store (`GET /v2/key-value-stores/{storeId}/records/{key}`). Check each row's `status`: `ok` rows have the transcript, `error` rows say why not.

### Pricing

Pay per event:

| Event | Price | When |
|---|---|---|
| `transcript` | $0.003 per transcript | each video whose non-empty transcript is delivered |
| `video-listed` | $0.0003 per video | each video row in the `metadata` mode (video details only, no transcript) |
| `apify-actor-start` | $0.0005 per run | once per run, one event per GB of memory with a 1 GB minimum (the default is 256 MB) |

Examples:

- A channel's newest 50 videos, 45 with captions: 45 x $0.003 + $0.0005 = $0.1355.
- 1000 videos across 20 channels, 900 with captions: 900 x $0.003 + $0.0005 = $2.7005.
- The video list of a channel, 500 videos, no transcripts: 500 x $0.0003 + $0.0005 = $0.1505.

Free: videos without captions, errors, blocked videos, filtered videos (date, length, Shorts, live), videos already delivered when `onlyNewSinceLastRun` is on, and sources that could not be listed. A video found by two sources is charged once. In a transcript run, listing is free: only delivered transcripts are charged.

### How it works, and what it does not do

- It lists videos the way youtube.com does for a visitor who is not signed in: a channel's public upload lists, a playlist page, or the search results page. No sign-in, no cookies, no browser.
- With `skipShorts` on, channels are read from YouTube's own long-form and past-live lists, so Shorts are left out exactly. In playlists, where YouTube does not mark Shorts, a video of 60 seconds or less counts as a Short. Live-now and upcoming videos are always skipped.
- Dates: listing pages show `3 weeks ago`, not a date. The actor turns that into the latest moment it can mean (`publishedAtIsEstimate: true`), so the `publishedAfter` cut keeps a borderline video rather than dropping it. A channel's 15 newest uploads get their exact time from the channel's public RSS feed.
- Transcripts come from the caption tracks YouTube itself publishes (the same engine as our [YouTube Transcript Scraper](https://apify.com/altronite/youtube-transcripts)). **No captions, no transcript**: it does not run speech-to-text.
- YouTube sometimes refuses cloud servers. Each video is retried on a fresh IP, then blocked videos get a second pass through residential IPs. A video still blocked after that returns `blocked`, free.
- `onlyNewSinceLastRun` keeps the delivered video ids per source in your account's named key-value store `youtube-bulk-transcripts-state`. Only delivered videos are remembered, so a new upload whose automatic captions are not ready yet is tried again next run. Delete that store to start over.
- Every video gets a row before the run's timeout; videos left when time runs out return `time_limit`, free.
- Only public data is collected. Transcripts can be copyrighted by the video owner; check your use is allowed.

### Troubleshooting

| `reason` | Meaning | What to do |
|---|---|---|
| `no_captions` | The video has no usable captions. | Turn on `allowAutoGenerated` if it is off. Not charged. |
| `not_found` | The channel, @handle or playlist does not exist, or the playlist is private. | Check the link. |
| `no_videos` | The source listed nothing at all. | Check the link or query. |
| `invalid_input` | An entry is not a channel, playlist or video reference. | See the message on the row. |
| `unavailable`, `unplayable`, `age_restricted` | Removed, private, members-only, region-locked or age-restricted video. | Not available without sign-in. |
| `blocked` | YouTube kept refusing the connection. | Run again later. Not charged. |
| `youtube_error` | A transient YouTube error. | Run again. |
| `time_limit` | The run hit its timeout first. | Raise the timeout in Run options or lower `maxVideosPerSource`. |
| `empty` | The caption track is empty. | Nothing to deliver; not charged. |
| `not_translatable`, `translation_unavailable` | YouTube can't translate that track. | Remove `translateTo`. |
| `failed` | Anything else. | Open an issue with the run id. |
| A source shows fewer videos than expected | Filters skipped them | See `SUMMARY`: `skipped` counts `short`, `live`, `too_old`, `too_short`, `too_long`. |

### FAQ

**How do I get all transcripts of a YouTube channel?**
Put the channel link or @handle in `channels` and set `maxVideosPerSource` to the number of videos you want (up to 5000). Uploads are read newest first.

**Can I get transcripts for a YouTube search?**
Yes. Put keywords in `searchQueries`; each query's results are listed and transcribed like a playlist.

**How do I get only new videos every day?**
Turn on `onlyNewSinceLastRun` and schedule the actor. Each run skips what earlier runs delivered for the same source, and you pay only for the new transcripts.

**Does it include Shorts?**
Not by default. Set `skipShorts: false` to include them.

**Why is `publishedAt` an estimate?**
YouTube's listing pages show relative dates. Exact times come from the channel RSS feed for the newest 15 uploads; others are estimated and flagged.

**Can it transcribe videos without captions?**
No. It reads existing caption tracks only, human-made or automatic.

### Limits

- Up to 5000 videos per source; a search returns as many results as YouTube serves for it.
- Mixes, Watch later, Liked videos, members-only and private content can't be read.
- Single videos have no duration, date or views (their title and channel come from YouTube's oEmbed data).

### Integrations

Call it from the Apify API, Python or JavaScript clients, Make, Zapier, n8n or an MCP client; get the dataset as JSON, CSV, Excel or XML, and the markdown files from the key-value store.

### Related tools

- [YouTube Transcript Scraper](https://apify.com/altronite/youtube-transcripts): transcripts for a list of video links.
- [Google Sheets Export & Import](https://apify.com/altronite/google-sheets-import-export): send any run's results to a Google Sheet.

### Support

Open an issue on the Issues tab with the source and the run id. Error rows and `SUMMARY` say why a video or source gave nothing, which usually answers it. Published by Altronite.

# Actor input Schema

## `channels` (type: `array`):

Channel URLs, @handles or channel ids (UC...). Uploads are read newest first.

## `playlists` (type: `array`):

Playlist URLs (with list=...) or playlist ids. Mixes (list=RD...) and private lists can't be read.

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

Keywords searched on YouTube (videos only). Each query is one source.

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

Video URLs (watch, youtu.be, Shorts, embed or live links) or 11-character video ids. Filters don't apply to these.

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

How many videos to take from each channel, playlist or search, after filters. Channels give their newest uploads.

## `searchSortBy` (type: `string`):

How YouTube orders search results.

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

Only videos published on or after this date (like 2026-01-01) or within a period (like 30 days). Listing pages show dates as "3 weeks ago", so the cut is approximate: a borderline video is kept, never dropped.

## `minDurationSec` (type: `integer`):

Skip videos shorter than this.

## `maxDurationSec` (type: `integer`):

Skip videos longer than this.

## `skipShorts` (type: `boolean`):

Leave out YouTube Shorts. Channels use YouTube's own Shorts split; in playlists, videos of 60 seconds or less count as Shorts.

## `onlyNewSinceLastRun` (type: `boolean`):

Skip videos this actor already delivered for the same source in an earlier run of your account (kept in the named key-value store youtube-bulk-transcripts-state). Videos that had no captions or failed are tried again. Good for scheduled channel monitoring.

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

Caption languages in order of preference, as codes like en, de or pt-BR. Human-made captions come before automatic ones. If none match, the video's first track is returned and flagged with languageFallback.

## `allowAutoGenerated` (type: `boolean`):

Use YouTube's automatic captions when no human-made track exists.

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

A language code like en or es. Uses YouTube's own caption translation when the track allows it.

## `outputFormat` (type: `string`):

Plain text is always in transcript rows. Video details only lists the videos (title, channel, date, duration, views) without fetching captions; it is charged per video row instead of per transcript.

## `markdownFiles` (type: `boolean`):

Also save one markdown file per source in the run's key-value store, with every transcript under its title: a ready corpus for an LLM or a RAG index.

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

How many videos are fetched at once, 1 to 10. Lower it if many videos come back as blocked.

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

YouTube limits requests from cloud servers. Apify Proxy is on by default; blocked videos are retried on a fresh IP and are never charged.

## Actor input object example

```json
{
  "channels": [
    "https://www.youtube.com/@3blue1brown",
    "@Fireship"
  ],
  "playlists": [
    "https://www.youtube.com/playlist?list=PLZHQObOWTQDPD3MizzM2xVFitgF8hE_ab"
  ],
  "searchQueries": [
    "python asyncio tutorial"
  ],
  "videos": [
    "https://www.youtube.com/watch?v=jNQXAC9IVRw"
  ],
  "maxVideosPerSource": 20,
  "searchSortBy": "relevance",
  "publishedAfter": "30 days",
  "minDurationSec": 120,
  "maxDurationSec": 3600,
  "skipShorts": true,
  "onlyNewSinceLastRun": false,
  "languages": [
    "es",
    "en"
  ],
  "allowAutoGenerated": true,
  "translateTo": "fr",
  "outputFormat": "text",
  "markdownFiles": true,
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

One row per video.

## `files` (type: `string`):

One markdown file per source and the SUMMARY record.

# 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 = {
    "channels": [
        "https://www.youtube.com/@jawed"
    ],
    "maxVideosPerSource": 5,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("altronite/youtube-bulk-transcripts").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 = {
    "channels": ["https://www.youtube.com/@jawed"],
    "maxVideosPerSource": 5,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("altronite/youtube-bulk-transcripts").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 '{
  "channels": [
    "https://www.youtube.com/@jawed"
  ],
  "maxVideosPerSource": 5,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call altronite/youtube-bulk-transcripts --silent --output-dataset

```

## MCP server setup

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

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/hm4eoYj8fQ2kE9gJv/builds/8CIcTOAASEuKzvpXY/openapi.json
