YouTube Transcript Scraper: Captions in Any Language avatar

YouTube Transcript Scraper: Captions in Any Language

Pricing

Pay per usage

Go to Apify Store
YouTube Transcript Scraper: Captions in Any Language

YouTube Transcript Scraper: Captions in Any Language

Get YouTube transcripts in the language you asked for, with one row per video and a clear status for every failure. Matches regional tracks (pt to pt-BR), flags machine translation, and outputs JSON, text, SRT, VTT or LLM-ready text. You pay only for transcripts retrieved.

Pricing

Pay per usage

Rating

0.0

(0)

Developer

Subrosa Systems Rosa

Subrosa Systems Rosa

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

19 days ago

Last modified

Categories

Share

YouTube Transcript Scraper

Pull the transcript out of any YouTube video that has captions, in the language you actually asked for, and get a straight answer about every video that doesn't work.

Feed it one URL or ten thousand. You get back one row per video, always, so a 500-item input produces a 500-row dataset you can diff against your input list. No silent drops.


Why this one

Three things go wrong with YouTube transcript scrapers. This Actor is built around fixing all three.

1. "It returned zero data."

The usual approach scrapes the YouTube watch page for caption URLs. Those URLs still look valid, but as of late 2026 they return HTTP 200 with an empty body. A scraper that trusts the status code reports success and writes you an empty dataset.

This Actor requests captions through YouTube's mobile player API instead, which still serves real caption data, and falls back to a second client if the first one is throttled. When a body does come back empty, it is reported as empty_transcript with retryable: true, never as "this video has no captions".

2. "I asked for Portuguese and got English."

YouTube labels caption tracks with regional codes: pt-BR, es-419, zh-Hans. A scraper comparing those against your "pt" finds no match, quietly falls back to the first track in the list, and hands you English while reporting success.

This Actor matches pt to pt-BR, es to es-419, handles pt_BR and PT-br, prefers human-written captions over auto-generated ones, and then tells you what it did in four fields:

FieldMeaning
languageRequestedwhat you asked for
languageReturnedwhat you actually got
languageMatchexact, base, translated, or default
translatedtrue if this is machine translation, not real captions

If no native track exists, you can let it fall back to YouTube's machine translation (clearly flagged) or refuse and get a clean language_unavailable status listing the languages that do exist.

3. "Something failed and I can't tell what."

Every failed video comes back with a machine-readable status, a plain-English message, and a retryable flag so your pipeline knows whether to try again or move on:

StatusRetry?Means
no_captionsnoVideo loaded fine; captions were never published
video_privatenoPrivate video
age_restrictednoNeeds a signed-in session
video_not_foundnoDeleted or never existed
language_unavailablenoRequested language absent, translation disabled
invalid_inputnoNot a resolvable YouTube URL or id
login_requirednoYouTube demanded a signed-in session. Usually means a datacenter IP; switch the proxy to residential
rate_limitedyesYouTube throttled the request. Use a proxy
empty_transcriptyesHTTP 200 with no content: throttling, not absence
fetch_failedyesNetwork or timeout

"No captions exist" and "we got throttled" are different problems with different fixes. Most scrapers report both as the same empty row.


Input

Paste YouTube links. Watch URLs, youtu.be short links, Shorts, embeds, live URLs, youtube-nocookie, or bare 11-character video ids all work, with or without extra query parameters.

{
"videoUrls": [
"https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"https://youtu.be/jNQXAC9IVRw?t=42",
"https://www.youtube.com/shorts/abc123XYZ99",
"kJQP7kiw5Fk"
],
"language": "pt",
"format": "json",
"allowTranslation": true,
"maxConcurrency": 5,
"proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }
}
OptionDefaultNotes
videoUrls(required)URLs or bare video ids
languagevideo defaultBCP-47: en, pt, pt-BR, es-419…
preferManualtrueHuman captions over auto-generated
allowTranslationtrueMachine-translate if no native track
formatjsonjson, text, srt, vtt, llm
includeMetadatatrueTitle, channel, duration, views
maxConcurrency51–20
requestTimeoutSecs20Per request
proxyConfigurationApify Proxy, residentialLeave on residential. See the note below

Output formats

format changes only the transcript field. Everything else stays identical.

json: timestamped cues, for search, alignment, or citation:

[
{ "start": 1.36, "duration": 1.68, "text": "Never gonna give you up" },
{ "start": 3.04, "duration": 1.5, "text": "Never gonna let you down" }
]

text: one clean paragraph, entities decoded, formatting tags stripped.

srt / vtt: ready-to-use subtitle files.

llm: paragraph-chunked with coarse time anchors, so a model can cite back to a moment in the video:

[00:00:01] Never gonna give you up Never gonna let you down …
[00:01:00] …

Example output row

{
"input": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"videoId": "dQw4w9WgXcQ",
"videoUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"status": "ok",
"success": true,
"title": "Rick Astley - Never Gonna Give You Up",
"channelName": "Rick Astley",
"durationSeconds": 213,
"viewCount": 1600000000,
"languageRequested": "pt",
"languageReturned": "pt-BR",
"languageMatch": "base",
"translated": false,
"autoGenerated": false,
"languageNote": "No exact \"pt\" track; used closest regional variant \"pt-BR\".",
"availableLanguages": [
{ "languageCode": "en", "name": "English", "autoGenerated": false },
{ "languageCode": "pt-BR", "name": "Portuguese (Brazil)", "autoGenerated": false }
],
"format": "json",
"cueCount": 61,
"characterCount": 1834,
"durationCoveredSeconds": 207.42,
"transcript": [ { "start": 1.36, "duration": 1.68, "text": "…" } ],
"error": null
}

And a failure, in the same dataset, in the same shape:

{
"input": "https://www.youtube.com/watch?v=aqz-KE-bpKQ",
"videoId": "aqz-KE-bpKQ",
"status": "no_captions",
"success": false,
"title": "Big Buck Bunny",
"transcript": null,
"error": {
"status": "no_captions",
"message": "This video has no caption tracks. The video itself loaded correctly. Captions were never published for it.",
"retryable": false
}
}

Pricing

$4 per 1,000 transcripts retrieved, plus a half-cent start fee per run.

You pay only for videos that come back with a transcript. A video with no captions, a private video, or one YouTube throttled is still written to the dataset with its status, and costs nothing. Set a maximum cost on the run and the Actor stops cleanly when it is reached.


Notes on running it

  • Keep the proxy on residential. This is the default and it matters more than it looks. YouTube answers datacenter IP addresses with a sign-in wall, so a datacenter proxy returns login_required on videos that are perfectly public. Residential addresses behave normally. If you switch proxy groups and suddenly see login_required everywhere, that is the cause.
  • Use a proxy for anything over ~50 videos. YouTube rate-limits by IP. Without one, a long run will start returning empty_transcript partway through. Apify Proxy is enabled by default.
  • Machine translation is throttled harder than native tracks. If you set language to something the video doesn't have, expect a lower success rate than for native captions. Those rows come back as empty_transcript with a message saying so, not as a false "no captions".
  • Live streams have no completed caption track until the broadcast ends.
  • Age-restricted and private videos cannot be read without a signed-in session. They are reported as such rather than failing vaguely.

Retrying only what's worth retrying

const { items } = await client.dataset(datasetId).listItems();
const worthRetrying = items
.filter((r) => r.error?.retryable)
.map((r) => r.videoUrl);

That's the whole point of the typed statuses: you never re-run a video that has no captions, and you never give up on one that was merely throttled.


This Actor reads publicly available caption data for videos you supply. It does not bypass authentication, does not access private or age-restricted content, and does not download video or audio. You are responsible for ensuring your use of the retrieved text complies with YouTube's Terms of Service and with the copyright applicable to the content you transcribe.

Questions or a video that behaves strangely? Open an issue on the Actor page. Issues get read and fixed.