YouTube Transcript Scraper: Captions in Any Language
Pricing
Pay per usage
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
Maintained by CommunityActor 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:
| Field | Meaning |
|---|---|
languageRequested | what you asked for |
languageReturned | what you actually got |
languageMatch | exact, base, translated, or default |
translated | true 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:
| Status | Retry? | Means |
|---|---|---|
no_captions | no | Video loaded fine; captions were never published |
video_private | no | Private video |
age_restricted | no | Needs a signed-in session |
video_not_found | no | Deleted or never existed |
language_unavailable | no | Requested language absent, translation disabled |
invalid_input | no | Not a resolvable YouTube URL or id |
login_required | no | YouTube demanded a signed-in session. Usually means a datacenter IP; switch the proxy to residential |
rate_limited | yes | YouTube throttled the request. Use a proxy |
empty_transcript | yes | HTTP 200 with no content: throttling, not absence |
fetch_failed | yes | Network 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"] }}
| Option | Default | Notes |
|---|---|---|
videoUrls | (required) | URLs or bare video ids |
language | video default | BCP-47: en, pt, pt-BR, es-419… |
preferManual | true | Human captions over auto-generated |
allowTranslation | true | Machine-translate if no native track |
format | json | json, text, srt, vtt, llm |
includeMetadata | true | Title, channel, duration, views |
maxConcurrency | 5 | 1–20 |
requestTimeoutSecs | 20 | Per request |
proxyConfiguration | Apify Proxy, residential | Leave 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_requiredon videos that are perfectly public. Residential addresses behave normally. If you switch proxy groups and suddenly seelogin_requiredeverywhere, 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_transcriptpartway through. Apify Proxy is enabled by default. - Machine translation is throttled harder than native tracks. If you set
languageto something the video doesn't have, expect a lower success rate than for native captions. Those rows come back asempty_transcriptwith 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.
Legal and usage
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.