YouTube Transcript Scraper API - Captions and Subtitles avatar

YouTube Transcript Scraper API - Captions and Subtitles

Pricing

from $5.92 / 1,000 transcript returneds

Go to Apify Store
YouTube Transcript Scraper API - Captions and Subtitles

YouTube Transcript Scraper API - Captions and Subtitles

Extracts YouTube transcripts (manual captions and auto-generated). Tells you whether a video is private, has no captions, or was blocked - instead of returning a blank row - and charges only for transcripts that actually contain text.

Pricing

from $5.92 / 1,000 transcript returneds

Rating

0.0

(0)

Developer

NeverEmpty

NeverEmpty

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

0

Monthly active users

6 days ago

Last modified

Categories

Share

Get the transcript of any public YouTube video — manual captions and auto-generated ones — as clean text, or as timestamped lines.

The difference is what happens when something goes wrong. Most transcript scrapers return an empty result whether the video has no captions, the video is private, or YouTube blocked the request. You cannot tell which, so you cannot fix it. This one tells you, in plain words, and only charges you for transcripts that actually contain text.

Measured, not claimed

A run of 15 videos on 2026-08-25, from the shipped build:

  • 14 returned a transcript. 83,647 characters total.
  • 1 did not — and it said why: "この動画は非公開です" (this video is private). Nobody can scrape that one. It was not silently dropped, and it was not charged.
  • 48.7 seconds for all 15.

What 180 videos actually returned

The 15-video run above is small, so on 2026-08-29 the same engine was pointed at the 15 newest uploads from 12 channels — 180 videos in all, picked from each channel's public feed rather than by hand.

OutcomeVideosShare
Transcript returned16390.6%
No caption track on the video at all158.3%
Video not playable (private, removed, region)21.1%

Of the 163 transcripts, 120 were auto-generated and 43 were manual captions. In other words, on a representative slice of active channels, three transcripts in four are machine transcription, and any claim that a scraper "gets the subtitles the creator wrote" is wrong three times out of four.

It varies enormously by channel

ChannelTranscriptsAuto-generatedNo caption track
Veritasium15 / 15110
Marques Brownlee15 / 1530
3Blue1Brown15 / 1580
Linus Tech Tips15 / 15150
TED15 / 1580
Computerphile15 / 15150
Fireship15 / 15150
NASA12 / 1511
ThePrimeagen14 / 15141
HIKAKIN14 / 15141
Kurzgesagt – In a Nutshell11 / 1594
Billie Eilish7 / 1578

Music channels are the outlier: more than half of the Billie Eilish uploads had no caption track at all. If your pipeline assumes "public video ⇒ transcript", a music-heavy input list will look broken.

(Channel names above are the titles YouTube itself returned for each handle. An earlier version of this measurement read the wrong channel id from the page and attributed videos to the wrong channels; the numbers here are from the corrected run.)

Asking for English does not guarantee English

The same 180-video run requested ["en"] every time.

  • 156 of 163 transcripts came back in English (95.7%).
  • 7 came back in another language — Arabic on a TED upload, Dutch on a Computerphile upload, and Japanese, Spanish and Vietnamese on HIKAKIN uploads.
  • In none of those 7 cases did the video have an English track that was passed over. When you get another language, it is because English was not there.

That is why every row carries language, isAutoGenerated and availableLanguages: so you can drop or translate the odd row instead of quietly mixing languages into a dataset you thought was English.

What you get

FieldExample
oktrue / false — did this video produce a transcript
videoId / urlaircAruvnKk
title / authorBut what is a neural network? / 3Blue1Brown
lengthSeconds1132
languageen
isAutoGeneratedfalse — manual captions are preferred when both exist
availableLanguages["en","es","fr","ja", ...]
segmentCount286
textThe whole transcript as one string
segments[{ "start": 12.4, "duration": 3.1, "text": "..." }, ...]
reasonWhy it failed, when ok is false

Why transcripts come back empty elsewhere

The caption URL published on a YouTube watch page returns zero bytes for every format — measured on every video tried. Anything built on that path silently produces nothing. This Actor does not use it. It reads the same endpoint the official mobile apps use, which still serves captions, and it requests only the fields it needs, so a lookup transfers about 19 KB instead of 228 KB.

That is also why it is cheap to run at volume.

Honest failure reasons

When a video does not produce a transcript, you get one of these in reason — never a blank row:

  • この動画は非公開です — private video
  • メンバー限定の動画です — members-only
  • 年齢制限のある動画です — age-restricted
  • この地域では再生できない動画です — geo-blocked
  • この動画に字幕がありません — the video genuinely has no captions
  • ボット判定でブロック — YouTube rate-limited the request; retried automatically with a fresh IP first

A private video and a blocked request are not the same problem, and this Actor does not pretend they are.

Input

{
"videos": [
"https://www.youtube.com/watch?v=aircAruvnKk",
"https://youtu.be/dQw4w9WgXcQ",
"M7lc1UVf-VE"
],
"languages": ["en"],
"includeTimestamps": true,
"proxyMode": "auto"
}

Watch URLs, youtu.be links, Shorts URLs, embed URLs, and bare 11-character IDs all work.

FieldDefaultMeaning
videos(required)URLs or IDs
languages["en"]Preferred order. Manual captions win over auto-generated
includeTimestampstrueInclude per-line segments alongside the full text
proxyModeautoauto tries datacenter first (cheaper), falls back to residential when YouTube blocks
maxRetries3Retries with a fresh IP on a block or a network failure

Calling it from your own code

JavaScript:

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('neverempty/youtube-transcript-reliable').call({
videos: ['https://www.youtube.com/watch?v=aircAruvnKk'],
languages: ['en'],
includeTimestamps: true,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const row of items) {
if (!row.ok) { console.warn(row.videoId, 'skipped:', row.reason); continue; }
console.log(row.title, '-', row.language, row.isAutoGenerated ? '(auto)' : '(manual)');
console.log(row.text.slice(0, 200));
}

Python:

from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("neverempty/youtube-transcript-reliable").call(run_input={
"videos": ["https://www.youtube.com/watch?v=aircAruvnKk"],
"languages": ["en"],
"includeTimestamps": True,
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
if not row["ok"]:
print(row["videoId"], "skipped:", row["reason"])
continue
print(row["title"], row["language"], "auto" if row["isAutoGenerated"] else "manual")

The same run is available over plain HTTP, and through the Apify MCP server if you are wiring it into an AI agent.

Feeding a transcript to an LLM

Two things matter and both are in the output.

Length. A 20-minute talk is roughly 3,000 words. Send text when you want a summary; send segments when the model has to cite a moment, because each segment carries start in seconds and you can turn that straight into a ?t= link back to the video.

Provenance. isAutoGenerated tells you whether the words were typed by a person or guessed by a recogniser. Auto-generated tracks have no punctuation on many channels and will mis-hear names — which is survivable for search and summarisation, and not survivable for quotation. Keep the flag on the row so the model, or the person reading its answer, can tell the difference.

Pricing

You are charged only when a transcript with actual text comes back. Private videos, videos without captions, and blocked requests cost you nothing.

That matters more than the headline number: a cheaper Actor that charges for empty rows costs more per usable transcript. On the 180-video run above, 17 videos produced nothing — a scraper that bills per input row would have charged for all 17.

Typical uses

  • Feeding video content to an LLM for summaries, Q&A or RAG
  • Searching across a channel's back catalogue
  • Subtitling, translation and repurposing workflows
  • Research and content analysis at scale

FAQ

Does it work on auto-generated captions? Yes. Manual captions are preferred when both exist, and isAutoGenerated tells you which you got. On the 2026-08-29 run, 120 of 163 transcripts were auto-generated.

What about videos in other languages? Pass languages in your order of preference. availableLanguages lists everything the video has. If you ask for English and the video has no English track, you get what it does have, and language says so.

Why does it need a proxy? YouTube rate-limits repeated caption requests from one address. Requests are retried from a fresh IP automatically. Residential proxying is used only when it is actually needed.

Will I be charged for a video with no captions? No. Charging happens after a transcript with text is produced.

Can it take a whole channel? This Actor takes a list of videos. For a channel, use YouTube Channel Transcript Scraper, which resolves the handle to a channel id first — handles get reused and redirected, so resolving them is not optional.

How many videos can I send at once? There is no fixed cap. The run is sequential with retries, so a list of a few hundred is routine; for tens of thousands, split the list across runs so a single block does not stall everything behind it.

Other tools by NeverEmpty

Every NeverEmpty Actor follows the same rule: it never returns an empty result to mean two different things, and it only charges for rows that actually carry an answer.

Support

If a video that should work does not, open an issue on the Issues tab with the video ID. The reason field on the failed row tells me most of what I need.