# YouTube Transcript Scraper (`garnet_puppet/youtube-transcript-scraper`) Actor

Extract YouTube transcripts with millisecond timestamps. Give it a video, a channel, a playlist or a search keyword and get raw caption segments plus merged paragraphs, ready for RAG, search and analysis. Incremental mode skips videos you already have, so a scheduled run does not bill you twice.

- **URL**: https://apify.com/garnet\_puppet/youtube-transcript-scraper.md
- **Developed by:** [Mehmet](https://apify.com/garnet_puppet) (community)
- **Categories:** AI, Developer tools, Videos
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $6.50 / 1,000 transcript scrapeds

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?

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

Give this Actor a YouTube video, Shorts, channel or playlist link — or just a search keyword —
and it returns the spoken content as structured, timestamped text: the raw caption lines
exactly as YouTube serves them, plus merged paragraphs ready for embedding. No browser
automation, no cookies, no `yt-dlp` binary to maintain.

*This Actor is an independent, third-party tool. It is not affiliated with, endorsed by, or
sponsored by YouTube or Google LLC. YouTube and the YouTube logo are trademarks of Google LLC.*

### What makes this different

Most transcript scrapers charge you again for every video, every run. This one
remembers what it already fetched.

**Incremental mode is on by default.** A video that was scraped in an earlier
run is skipped and **not charged**. If you track channels on a schedule, this
is where the actual bill is decided:

| Tracking 20 channels daily, 30 videos each | Transcripts billed per month |
|---|---|
| A scraper without deduplication | 18,000 |
| This Actor, incremental mode on | **660** |

That is **27× fewer billed units** for the same coverage, assuming each channel
publishes around three new videos a month. The ratio is the point: it holds
whatever the two unit prices happen to be.

**This applies to recurring runs.** For a single one-off bulk scrape where
every video is new, a cheaper per-unit actor will cost you less. Pick the tool
that matches your job.

Three more things you get here:

- **Paragraphs, not just caption fragments.** Raw captions arrive as 2-second
  snippets. `chunkSeconds` merges them into timestamped paragraphs that are
  ready to embed. Set `chunkBy` to `chapter` and the split follows the video's
  own chapters when it has them.
- **Human-written captions preferred.** `preferManual` picks the human
  transcript over the auto-generated one when both exist. Auto captions have no
  punctuation and need cleaning before they are useful.
- **Translate the transcript.** Set `translateTo` to a language code and the
  transcript comes back in that language, using YouTube's own translation — no
  extra request, no extra charge. Every video supports a different set of
  translation languages; if yours doesn't offer the one you asked for, you
  still get the original transcript plus a warning listing what it does
  support, and `translatedTo` on the record tells you at a glance whether the
  translation actually happened.

### Check the cost before you spend it

Turn on **Preview only** and the actor lists every video it would scrape, how
many were already fetched in an earlier run, and how many would actually be
charged — without fetching a single transcript.

> Preview makes **no transcript charges**. The Apify actor start event is still
> billed, because the platform charges it before this actor runs. There is no
> way for any actor to suppress it.

### Judging transcript quality in bulk

Every record carries a `transcriptQuality` block so you can filter before you
ingest:

| Field | Use it to |
|---|---|
| `coverageRatio` | Drop videos where captions cover only part of the video |
| `wordCount`, `wordsPerMinute` | Spot near-empty or broken transcripts |
| `hasPunctuation` | Decide which transcripts need cleaning first |
| `isAutoGenerated` | Prefer human-written text when you have a choice |

### Output formats

`outputFormat` adds a ready-to-use string to each record: `text`, `srt` or
`vtt`. Segments, paragraphs and metadata are **always** included as well — you
never have to choose between a subtitle file and structured data.

### Chapters

When a video's description contains a proper chapter list, `chapters` is filled
in and `chunkBy: chapter` splits paragraphs on those boundaries. Many videos do
not have one; in our sample only about a quarter did. When there is no chapter
list the actor falls back to fixed-length paragraphs.

### What does YouTube Transcript Scraper do?

Paste any mix of links into **YouTube URLs** and get back one dataset record per video: title,
channel, and thumbnail, plus duration, view count, publish date, and the transcript when
**Include transcript** is on. Video and Shorts links are scraped directly; channel and playlist
links (`@handle`, `/channel/UC...`, or `list=...`) are expanded into their videos automatically,
up to a configurable cap. No link handy? Use **Search keywords** instead and the Actor searches
YouTube for you — no URL needed. Every record carries `inputUrl`, so you always know which
source produced it, even when several sources are mixed in one run.

Transcripts come in two forms. `segments` are the caption lines exactly as YouTube's track
returns them. `chunks` merge those lines into paragraph-length blocks, split at sentence
boundaries for human-written captions and at pause gaps for auto-generated ones. Every chunk
and segment carries a millisecond `startMs`.

Click **Start** to see it run against two known-good example videos — no setup required.

### Sample output

Most of this record is copied from a real run of this Actor (Run `mG13RBc8ty3X2ltoN`, dataset
`k4Jj0ETl2bDe7h5cw`). To respect copyright, most `segments` and all `chunks` text are
collapsed below. Six fields — `inputUrl`, `description`, `keywords`, `chapters`,
`transcriptQuality` and `translatedTo` — were added to the schema after that run and are shown
here with illustrative example values rather than that run's actual data; `transcriptFormatted`
is genuinely `null` below because `outputFormat` defaults to `json`, and `translatedTo` is
genuinely `null` because no translation was requested.

```json
{
  "videoId": "arj7oStGLkU",
  "url": "https://www.youtube.com/watch?v=arj7oStGLkU",
  "inputUrl": "https://www.youtube.com/watch?v=arj7oStGLkU",
  "title": "Inside the Mind of a Master Procrastinator | Tim Urban | TED",
  "channelName": "TED",
  "channelUrl": "https://www.youtube.com/@TED",
  "thumbnailUrl": "https://i.ytimg.com/vi/arj7oStGLkU/hqdefault.jpg",
  "durationSeconds": 844,
  "viewCount": 61678070,
  "publishedAt": "2016-04-06T16:59:35.000Z",
  "description": "/* the video's own description text, truncated */",
  "keywords": ["TED", "TEDTalk", "..." /* illustrative, truncated */],
  "language": "en",
  "isAutoGenerated": false,
  "availableLanguages": ["ar", "hy", "bg", "..." /* 49 total, truncated */],
  "segments": [
    { "startMs": 12645, "durationMs": 1370, "text": "So in college," }
    // 314 more segments — truncated for this README
  ],
  "chunks": [
    { "startMs": 12645, "endMs": 50900, "text": "/* 608-char merged paragraph, truncated */", "language": "en", "isAutoGenerated": false }
    // 21 more chunks — truncated for this README
  ],
  "nonSpeechEvents": [],
  "chapters": [],
  "transcriptAvailable": true,
  "transcriptQuality": {
    "coverageRatio": 0.97,
    "wordCount": 2180,
    "wordsPerMinute": 155,
    "hasPunctuation": true
  },
  "transcriptFormatted": null,
  "scrapedAt": "2026-08-10T19:15:12.912Z",
  "translatedTo": null,
  "contentHash": "ba594315dc2da412ed55e0ae1e6610be77895fa6e08e6c1bf63c966d924d5de1"
}
```

The real record has 315 `segments` and 22 `chunks` for this 14-minute talk. `nonSpeechEvents`
is empty here; on a music video it holds entries like `[Music]`, kept separate from spoken
text. `chapters` is empty above because this video has none — see the Chapters section above.

### How to use YouTube Transcript Scraper

1. Click **Start** to run it with the two example videos, or replace **YouTube URLs** with your
   own — video links, Shorts links, `youtu.be` short links, bare video IDs, channel links
   (`@handle` or `/channel/UC...`), or playlist links all work in the same field.
2. No link on hand? Add terms to **Search keywords** instead and the Actor finds and scrapes
   the top results for you.
3. Pulling a channel or playlist? Set **Max videos per source** to cap how many it expands to.
4. Not sure what a run will cost? Turn on **Preview only** first — see "Check the cost before
   you spend it" above.
5. Leave **Incremental mode** on and schedule the run — each later run skips videos already
   fetched, so a daily schedule only pays for what's new.
6. Read results from the dataset, the API, or push them into Google Sheets, Slack, Zapier,
   Make or n8n through the standard Apify integrations.

### Input configuration

| Field | What it does | Default |
|---|---|---|
| YouTube URLs | Any mix of video, Shorts, channel or playlist links. Channel and playlist links are expanded into their videos automatically | 2 example videos |
| Search keywords | Search YouTube for each keyword and scrape transcripts from the top results — no URL needed | Empty |
| Preview only (no transcript charges) | List what would be scraped and how many videos would be charged, without fetching a single transcript | Off |
| Preferred languages | Language codes in priority order (`en`, `de`, `tr`, ...). If none match, the first available track is returned rather than skipped — check `language` on the record | `["en"]` |
| Prefer human-written captions | Choose manual captions over auto-generated when both exist | On |
| Translate transcript to | Language code to translate the transcript into (`es`, `de`, ...), using YouTube's own translation at no extra charge. Falls back to the original transcript with a warning if the video doesn't offer that language — check `translatedTo` on the record | Off (no translation) |
| Include transcript | Off skips duration, view count and publish date; title, channel and thumbnail return, at the lower price | On |
| Transcript format | Adds a ready-to-use `text`, `srt` or `vtt` string to each record. Segments and paragraphs are always included as well | JSON only |
| Paragraph length (seconds) | Target length of each merged chunk; raw segments always kept too | `40` (5–600) |
| Paragraph boundaries | Split paragraphs on the video's own chapters when it has them, otherwise fixed length | Fixed length |
| Include Shorts | When expanding a channel, also include Shorts. Shorts links given directly are always scraped | Off |
| Max videos per source | Upper bound on videos pulled per channel, playlist or search | `30` (1–200) |
| Max pages per source | How many result pages to fetch per channel, playlist or search | `10` (1–20) |
| Incremental mode | Skip videos already fetched in a previous run | On |

**Video URLs**, **Channel URLs** and **Max videos per channel** are deprecated but still
accepted, so existing runs keep working — new runs should use **YouTube URLs** and **Max
videos per source** instead.

### Pricing

This Actor uses Apify's Pay-Per-Event model. Two events fire independently:

| Event | Fires when |
|---|---|
| `video-metadata` | A video record is delivered — even with no captions |
| `transcript-scraped` | A transcript was actually extracted and included |

**A video with no captions, a deleted video, or one that stays blocked after retries never
triggers `transcript-scraped`.** A caption-less video still returns a metadata-only record,
charged at the metadata rate only. A deleted, private, or permanently blocked video returns no
record at all and is not charged for either event — you only pay the transcript rate for
transcripts you actually receive. Turn **Include transcript** off to fetch metadata only, at
the lower single-event rate. Exact per-event prices are on this Actor's Pricing tab in Apify
Console.

### Limits, stated plainly

**Not every video has a transcript, and we do not invent one.**

- **No captions at all** — the video still returns a metadata record, charged at the metadata
  rate only. `transcript-scraped` does not fire.
- **Deleted, private, or permanently blocked** — no record at all, and no charge for either
  event.
- **Auto-generated captions carry YouTube's own errors.** Proper nouns, technical terms and
  overlapping speech are where they break down. The record tells you whether the track was
  auto-generated or human-written, so you can filter on it.
- **Translation is YouTube's, not ours.** If the video does not offer the language you asked
  for, you get the original transcript plus a warning — check `translatedTo` on the record
  rather than assuming the translation happened.
- **Shorts and long videos behave the same way**, but a video that stays rate-limited after
  retries is reported as such instead of being silently dropped.

### Output & integrations

Every record follows one schema: `videoId`, `url`, `inputUrl`, `title`, `channelName`,
`channelUrl`, `thumbnailUrl`, `durationSeconds`, `viewCount`, `publishedAt`, `description`,
`keywords`, `language`, `isAutoGenerated`, `availableLanguages`, `segments`, `chunks`,
`nonSpeechEvents`, `chapters`, `transcriptAvailable`, `transcriptQuality`,
`transcriptFormatted`, `scrapedAt`, `translatedTo`, `contentHash`.

`segments` and `chunks` exist side by side on purpose. `segments` are the unmodified caption
lines, for your own paragraphing or subtitle format. `chunks` are pre-merged into paragraphs
sized by **Paragraph length**, ready for search-index or RAG pipelines without extra work.
Both carry millisecond timestamps, so any chunk or segment links back to `url` plus its
`startMs` to jump into the video.

`inputUrl` records which entry in **YouTube URLs** or **Search keywords** produced this video —
useful when one run mixes several sources. `chapters` holds the video's own chapter list when it
has one (see "Chapters" above) and is empty otherwise. `transcriptQuality` is the quality block
described in "Judging transcript quality in bulk" above. `transcriptFormatted` holds the
ready-to-use string requested via **Transcript format**, and is `null` when that setting is
left at its `json` default.

`isAutoGenerated` tells you whether captions came from YouTube's speech model or a human —
useful for quality filtering, since auto captions are noisier on accented or technical speech.
`translatedTo` holds the language code the transcript was actually translated into when
**Translate transcript to** succeeded, and is `null` when no translation was requested or the
video didn't offer the one you asked for — `language` always keeps carrying the *source*
track's language, so the two fields together answer "was this translated, and from what."
`contentHash` fingerprints every field except `scrapedAt` (the fetch timestamp), `inputUrl`
(provenance, not content) and `transcriptFormatted` (derived from `segments`, not independent
content) — useful for detecting changes between your own runs without diffing every field.
A translated transcript is genuinely different content, so `translatedTo` is included in the
hash: translating a previously-scraped video produces a new `contentHash`.
`inputUrl` and `transcriptFormatted` were excluded after earlier versions of this Actor already
shipped, so records produced before this version will hash differently even when nothing about
the video changed — expect one full sweep of "changed" records the first time you run this
version.

**Incremental mode** works by video ID, and only for runs where **Include transcript** is on:
once a video has produced a transcript record, it's skipped — and not re-charged — on a later
run. Metadata-only runs (**Include transcript** off) are never recorded this way, so the same
videos are billed again on every run.

Export as JSON, CSV, Excel or XML, or read the dataset through the Apify API. Works with the
standard Apify integrations: Google Sheets, Slack, Zapier, Make, n8n, and webhooks.

### Frequently asked questions

**Is this Actor affiliated with YouTube?**
No. This is an independent, unofficial tool, not affiliated with, endorsed by, or connected
to YouTube or Google LLC. All product names and trademarks belong to their respective owners.

**Why do some videos return no transcript?**
Two outcomes return no transcript: no captions in any language (metadata rate only), or a
deleted, private, or blocked video after three retries (nothing charged). Captions that don't
match your **Preferred languages** aren't skipped — the Actor falls back to the first available
track, charged at the transcript rate.

**Are auto-generated captions accurate?**
It varies — auto captions handle clear speech well but struggle with fast talkers, technical
terms, or heavy accents. Check `isAutoGenerated` on the record to filter or clean up as needed.
This Actor prefers human-written captions by default, via **Prefer human-written captions**.

**Can I get a whole channel?**
Yes. Paste the channel's `@handle` or `/channel/UC...` URL into **YouTube URLs** and set **Max
videos per source** (up to 200). The Actor pages through the upload list and processes every
video the same way as a single one — same metadata, transcript logic, and incremental skip.
Playlists and search results work the same way. If a source has more videos than your cap, the
run finishes normally and its `RUN-SUMMARY` output notes the truncation.

# Actor input Schema

## `urls` (type: `array`):

Any mix of YouTube links. Video and Shorts links are scraped directly; channel and playlist links are expanded into their videos automatically.

## `searchKeywords` (type: `array`):

Search YouTube for each keyword and scrape transcripts from the top results. No video URL needed.

## `previewOnly` (type: `boolean`):

List which videos would be scraped and how many would be charged, without fetching any transcript. Only the actor start event is billed; no transcript charges are made.

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

Adds a ready-to-use transcript string to each record. Timestamps, segments and paragraphs are always included as well.

## `scrapeShorts` (type: `boolean`):

When expanding a channel, also include Shorts, added after regular videos. Max videos per source applies to the combined list, so a low limit may leave no room for Shorts. Shorts links given directly are always scraped.

## `chunkBy` (type: `string`):

Chapter mode splits paragraphs on the video's own chapters when it has them, and falls back to fixed length otherwise.

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

Upper limit of videos taken from each channel, playlist or search. Defaults to 30.

## `maxPagesPerSource` (type: `integer`):

How many result pages to fetch from each channel, playlist or search.

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

Deprecated: use the "YouTube URLs" field instead. Still accepted so existing runs keep working. YouTube video links or IDs. Leave empty to use the example videos.

## `channelUrls` (type: `array`):

Deprecated: use the "YouTube URLs" field instead. Still accepted so existing runs keep working. Channel links. Both forms work: https://www.youtube.com/@handle and https://www.youtube.com/channel/UC... The most recent videos are processed.

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

Language codes in order of preference, for example en, de, tr. Falls back to the first available track.

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

Language code to translate the transcript into, for example es or de. Uses YouTube's own translation, so no extra cost. If a video does not offer that language, the original transcript is returned and a warning lists what it does offer.

## `preferManual` (type: `boolean`):

Human-written captions are cleaner than auto-generated ones. Turn off to accept whichever comes first.

## `includeTranscript` (type: `boolean`):

Turn off to fetch only title, channel and thumbnail at the lower metadata price.

## `chunkSeconds` (type: `integer`):

Target length of each merged paragraph. Raw segments are always returned as well.

## `maxVideosPerChannel` (type: `integer`):

Deprecated: use the "Max videos per source" field instead. Still accepted so existing runs keep working. Upper limit of videos taken from each channel. Defaults to 30.

## `incremental` (type: `boolean`):

Skip videos already fetched with a transcript in a previous run. Keeps scheduled runs cheap. Metadata-only runs (Include transcript off) are not recorded this way and are charged again every time.

## Actor input object example

```json
{
  "urls": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "https://www.youtube.com/watch?v=arj7oStGLkU"
  ],
  "searchKeywords": [],
  "previewOnly": false,
  "outputFormat": "json",
  "scrapeShorts": false,
  "chunkBy": "time",
  "maxPagesPerSource": 10,
  "channelUrls": [],
  "languages": [
    "en"
  ],
  "preferManual": true,
  "includeTranscript": true,
  "chunkSeconds": 40,
  "incremental": true
}
```

# Actor output Schema

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

One item per video: identity fields, caption segments, merged paragraphs, chapters and transcript quality metrics.

## `runSummary` (type: `string`):

Counts for the whole run: what was delivered, what was skipped, what failed, and any warnings. Read this to see whether the run did what you expected.

# 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 = {
    "urls": [
        "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
        "https://www.youtube.com/watch?v=arj7oStGLkU"
    ]
};

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

# Run the Actor and wait for it to finish
run = client.actor("garnet_puppet/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 '{
  "urls": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "https://www.youtube.com/watch?v=arj7oStGLkU"
  ]
}' |
apify call garnet_puppet/youtube-transcript-scraper --silent --output-dataset

```

## MCP server setup

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