YouTube Transcript Scraper (+ Metadata) avatar

YouTube Transcript Scraper (+ Metadata)

Pricing

from $2.25 / 1,000 transcripts

Go to Apify Store
YouTube Transcript Scraper (+ Metadata)

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

Zee Lex

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

4 days ago

Last modified

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"
}
FieldDefaultNotes
videoUrls–watch, youtu.be, /shorts/, /live/, embed URLs or 11-char IDs
channelUrls–latest uploads of each channel
playlistUrls–in playlist order
maxVideosPerChannel / maxVideosPerPlaylist20
languageenen also matches en-GB, en-US…
fallbackToAnyLanguagetrueotherwise status no_transcript
translateTo–YouTube auto-translate (tlang), when offered
preferManualCaptionstruefalse prefers auto-generated
outputFormatbothsegments, text, both
includeMetadatatruefalse = transcript only, fewer requests
maxConcurrency10
proxyConfigurationApify datacenterblocked 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

statusMeaningCharged
oktranscript returnedyes
no_transcriptvideo has no captions, none in your language (with fallback off), or it's an upcoming premiere/streamno
privateprivate videono
unavailableremoved, region-blocked, members-only or otherwise unplayableno
age_restrictedneeds a signed-in, age-verified accountno
errorstill 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:

  • start and duration are in seconds. duration is how long the caption is on screen, so captions can overlap.
  • publishedAt is ISO 8601 (UTC). likeCount is null when the channel hides likes or YouTube doesn't show the count.
  • isLive is true for videos that are or were live streams.
  • isShort uses a lightweight check that runs only for videos of 3 minutes or less (or a /shorts/ input).
  • wordCount counts whitespace-separated words, so it isn't meaningful for Chinese or Japanese.
  • With includeMetadata: false, all metadata fields except title are null.

Run summary

RUN_SUMMARY in the key-value store (linked from the run's Output tab) contains:

  • overallStatus: ok, partial or failed
  • 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 ok items

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

  1. 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.
  2. 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-source youtube-transcript-api.
  3. Downloads the chosen track as json3, falling back to the default XML and then srv3. It decodes entities and merges the segments into plain text.
  4. Channels are turned into their uploads playlist (UC… → UU…). Playlists are read through InnerTube browse, following continuations.
  5. 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 in RUN_SUMMARY are the early warning that YouTube changed something.

Pricing

Apify planPrice 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 install
npm test # unit tests (node --test) on saved fixtures
npm 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 to test/fixtures/live/, and npm test then also runs test/live-fixtures.test.ts on them. The fixtures in test/fixtures/ are synthetic; make_fixtures.py builds them from the shapes used by youtube-transcript-api and youtubei.js.
  • Offline end-to-end run: npm run mock-server starts a local stand-in covering every status, a bot check, a PO-token fallback, a channel with two pages and a missing playlist. Then run YT_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.