YouTube Transcript Scraper (+ Metadata)
Pricing
from $2.25 / 1,000 transcripts
YouTube Transcript Scraper (+ Metadata)
Get full YouTube transcripts (timestamped segments and plain text) plus video metadata from videos, channels and playlists. Manual or auto-generated captions, language fallback and auto-translation. Built for AI/RAG, summaries, SEO and content repurposing.
Pricing
from $2.25 / 1,000 transcripts
Rating
0.0
(0)
Developer
Zee Lex
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
4 days ago
Last modified
Categories
Share
Get the full transcript of any YouTube video, as timestamped segments and as clean plain text, together with key video metadata. Works on single videos, whole channels and playlists. It's built for AI/RAG pipelines, summaries, SEO and content repurposing.
- Videos, Shorts, live replays, channels (
@handle,/channel/UC…,/c/…,/user/…) and playlists - Manual (creator-uploaded) or auto-generated captions, your choice
- Preferred language with a fallback to any available language, plus YouTube's own auto-translation (
translateTo) - Every video gets a result with a clear
status, so you can see why a video has no transcript - You pay only for videos that return a transcript
- Fast HTTP scraping with no browser, at 512 MB of memory
Use cases
- AI / RAG: feed transcripts into vector databases, chatbots and agents; segments keep timestamps for citations
- Summaries: summarise lectures, podcasts and talks with an LLM
- SEO and repurposing: turn videos into blog posts, show notes, newsletters, social posts and quotes
- Research: analyse what a channel or a playlist talks about, at scale
Input examples
A single video
{ "videoUrls": ["https://www.youtube.com/watch?v=aircAruvnKk"] }
A channel's latest 50 videos, plain text only
{"channelUrls": ["https://www.youtube.com/@3blue1brown"],"maxVideosPerChannel": 50,"outputFormat": "text"}
A playlist, transcripts in Spanish (auto-translated where needed)
{"playlistUrls": ["https://www.youtube.com/playlist?list=PLZHQObOWTQDNU6R1_67000Dx_ZCJB-3pi"],"maxVideosPerPlaylist": 100,"language": "es","translateTo": "es"}
| Field | Default | Notes |
|---|---|---|
videoUrls | – | watch, youtu.be, /shorts/, /live/, embed URLs or 11-char IDs |
channelUrls | – | latest uploads of each channel |
playlistUrls | – | in playlist order |
maxVideosPerChannel / maxVideosPerPlaylist | 20 | |
language | en | en also matches en-GB, en-US… |
fallbackToAnyLanguage | true | otherwise status no_transcript |
translateTo | – | YouTube auto-translate (tlang), when offered |
preferManualCaptions | true | false prefers auto-generated |
outputFormat | both | segments, text, both |
includeMetadata | true | false = transcript only, fewer requests |
maxConcurrency | 10 | |
proxyConfiguration | Apify datacenter | blocked requests escalate to RESIDENTIAL |
Output sample
One dataset item per video:
{"videoId": "aircAruvnKk","url": "https://www.youtube.com/watch?v=aircAruvnKk","title": "But what is a neural network? | Deep learning chapter 1","channelName": "3Blue1Brown","channelId": "UCYO_jab_esuFRV4b17AJtAw","channelUrl": "https://www.youtube.com/@3blue1brown","publishedAt": "2017-10-05T15:11:25.000Z","durationSeconds": 1120,"viewCount": 25009905,"likeCount": 568125,"description": "What are the neurons, why are there layers, and what is the math underlying it?\nHelp fund future projects: https://www.patreon.com/3blue1brown\nWritten...","thumbnailUrl": "https://i.ytimg.com/vi_webp/aircAruvnKk/maxresdefault.webp","isLive": false,"isShort": false,"transcriptLanguage": "en","transcriptLanguageName": "English","isAutoGenerated": false,"isTranslated": false,"availableLanguages": [{"languageCode": "ar","languageName": "Arabic","isAutoGenerated": false},{"languageCode": "bn","languageName": "Bangla","isAutoGenerated": false},{"languageCode": "zh","languageName": "Chinese","isAutoGenerated": false}],"transcript": [{"start": 4.22,"duration": 1.18,"text": "This is a 3."},{"start": 6.06,"duration": 4.653,"text": "It's sloppily written and rendered at an extremely low resolution of 28x28 pixels,"},{"start": 10.713,"duration": 3.007,"text": "but your brain has no trouble recognizing it as a 3."}],"transcriptText": "This is a 3. It's sloppily written and rendered at an extremely low resolution of 28x28 pixels, but your brain has no trouble recognizing it as a 3. And I want you to take a moment to appreciate how c...","wordCount": 3357,"status": "ok","error": null}
The sample values are illustrative.
Statuses
status | Meaning | Charged |
|---|---|---|
ok | transcript returned | yes |
no_transcript | video has no captions, none in your language (with fallback off), or it's an upcoming premiere/stream | no |
private | private video | no |
unavailable | removed, region-blocked, members-only or otherwise unplayable | no |
age_restricted | needs a signed-in, age-verified account | no |
error | still blocked or failing after 4 retries (technical) | no |
error explains non-ok statuses. It's also set on ok items when a fallback was used, e.g. No transcript in "en"; returned "de".
Field notes:
startanddurationare in seconds.durationis how long the caption is on screen, so captions can overlap.publishedAtis ISO 8601 (UTC).likeCountisnullwhen the channel hides likes or YouTube doesn't show the count.isLiveistruefor videos that are or were live streams.isShortuses a lightweight check that runs only for videos of 3 minutes or less (or a/shorts/input).wordCountcounts whitespace-separated words, so it isn't meaningful for Chinese or Japanese.- With
includeMetadata: false, all metadata fields excepttitlearenull.
Run summary
RUN_SUMMARY in the key-value store (linked from the run's Output tab) contains:
overallStatus:ok,partialorfailed- totals per status, number of charged transcripts and whether your cost limit was reached
- retries and their reasons, and escalations to residential proxy
- where captions came from (watch page or InnerTube client) and in which format (json3, srv1, srv3)
- per channel/playlist results (resolved ID, videos queued, errors)
- invalid inputs
- % missing for every field across
okitems
The run fails (non-zero exit) only if every input failed for technical reasons. Videos without captions don't fail the run. Channels or playlists that don't exist are reported in the summary as bad input.
How it works
- Loads the watch page with consent cookies set, so the EU consent page is skipped. It reads metadata, the playability status and the caption tracks from
ytInitialPlayerResponse. - If the page's caption links need a PO token (
&exp=xpe) or there are none, it asks YouTube's InnerTube player API, as the ANDROID client first, then the WEB client. This is the same approach as the open-sourceyoutube-transcript-api. - Downloads the chosen track as
json3, falling back to the default XML and thensrv3. It decodes entities and merges the segments into plain text. - Channels are turned into their uploads playlist (
UC…→UU…). Playlists are read through InnerTubebrowse, following continuations. - Bot checks ("Sign in to confirm you're not a bot"), reCAPTCHA pages, 429/403/5xx and empty responses mark the session as bad. The request is retried on a new session with exponential backoff (2, 4, 8, 16 s), and from the 2nd retry it goes through residential proxy.
Limitations
- YouTube actively fights scraping. It increasingly requires "PO tokens" for caption downloads and shows bot checks to datacenter IPs. This Actor works around that with the ANDROID client and residential escalation, but YouTube can change this at any time.
- Age-restricted, private and members-only videos need a signed-in account, and this Actor never logs in.
- Auto-translation exists only when YouTube offers it for that video. The translations are machine-made.
- Auto-generated captions have no punctuation in many languages and may contain recognition errors.
- Channel scraping reads the channel's uploads playlist, which includes Shorts and past live streams in upload order.
- All YouTube-specific paths and request shapes are in
src/mappings.ts. Rising "missing" percentages inRUN_SUMMARYare the early warning that YouTube changed something.
Pricing
| Apify plan | Price per 1,000 transcripts |
|---|---|
| Free | $3.00 |
| Bronze | $2.75 |
| Silver | $2.50 |
| Gold+ | $2.25 |
Video metadata is included at no extra cost, and you only pay for videos that actually return a transcript — videos without captions, private or unavailable videos are free. This Actor is built for pay-per-event pricing with a single custom event, transcript (the CHARGE_EVENT constant in src/mappings.ts). That event is charged once per video with status ok. When setting up monetization, don't enable the per-dataset-item event, or videos without transcripts would be charged too. The Actor stops as soon as the user's maximum cost per run is reached.
Development
npm installnpm test # unit tests (node --test) on saved fixturesnpm run build && apify run # local run with storage/key_value_stores/default/INPUT.json
- Check against real YouTube: where youtube.com is reachable, run
npm run capture-fixtures. It saves real watch, player, timedtext and browse responses totest/fixtures/live/, andnpm testthen also runstest/live-fixtures.test.tson them. The fixtures intest/fixtures/are synthetic;make_fixtures.pybuilds them from the shapes used by youtube-transcript-api and youtubei.js. - Offline end-to-end run:
npm run mock-serverstarts a local stand-in covering every status, a bot check, a PO-token fallback, a channel with two pages and a missing playlist. Then runYT_BASE_URL=http://127.0.0.1:4577 node dist/main.js.
Request shapes and paths are ported from youtube-transcript-api v1.2.4 and youtubei.js v18.3.0, both MIT-licensed. Neither is a runtime dependency.