YouTube Transcript Scraper
Pricing
from $1.40 / 1,000 transcript (one per video result)s
YouTube Transcript Scraper
Get the transcript of any YouTube video by URL or ID. Returns plain text and timed segments in any available caption language, with SRT and VTT output, auto vs manual caption flags and chapters. No API key needed.
Pricing
from $1.40 / 1,000 transcript (one per video result)s
Rating
0.0
(0)
Developer
Tarik Cerkezovic
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Security
Limited permissionsShare
Get the transcript of any YouTube video as plain text and timed segments. Give it video URLs or IDs, pick a language, and get back the captions the video actually has, with the title, channel, duration, chapters and a flag telling you whether the captions were written by the creator or generated by YouTube. No API key, no cookies, no browser.
Built for AI pipelines, research and content tools: array input, one item per video, clean error codes instead of failed runs, and SRT or WebVTT when you need subtitle files.
What you get
For every video, one dataset item with:
| Field | Description |
|---|---|
videoId, url, title, channelName, channelId, durationSeconds, viewCount | Video metadata |
text | The full transcript as one string |
segments | Timed lines: start, duration, end in seconds and text |
language, languageName, captionKind | Which track was returned; captionKind is manual (creator-uploaded) or auto (YouTube speech recognition) |
availableLanguages, translatableTo | Every caption track the video offers, and the languages YouTube can auto-translate it into |
chapters | Chapters parsed from the description timestamps, when the creator added them |
wordCount, segmentCount | Sizes |
srt, vtt | Subtitle files as strings, when you switch them on |
error, errorMessage | Present only on items that could not be fetched, see below |
Sample output
{"videoId": "jNQXAC9IVRw","url": "https://www.youtube.com/watch?v=jNQXAC9IVRw","title": "Me at the zoo","channelName": "jawed","channelId": "UC4QobU6STFB0P71PMvOGN5A","durationSeconds": 19,"viewCount": 447816985,"language": "en","languageName": "English","captionKind": "manual","text": "All right, so here we are, in front of the elephants the cool thing about these guys is that they have really... really really long trunks and that's cool and that's pretty much all there is to say","wordCount": 39,"segmentCount": 6,"segments": [{ "start": 1.2, "duration": 2.16, "end": 3.36, "text": "All right, so here we are, in front of the elephants" },{ "start": 3.36, "duration": 2.4, "end": 5.76, "text": "the cool thing about these guys" }],"chapters": [{ "title": "Intro", "startSeconds": 0 },{ "title": "The cool thing", "startSeconds": 5 },{ "title": "End", "startSeconds": 17 }],"availableLanguages": [{ "code": "en", "name": "English", "kind": "manual" },{ "code": "de", "name": "German", "kind": "manual" }],"translatableTo": ["ar", "bs", "de", "es", "fr", "ja"],"source": "android","fetchedAt": "2026-11-05T14:47:26.123Z","input": "https://www.youtube.com/watch?v=jNQXAC9IVRw"}
Input
| Field | Type | Default | What it does |
|---|---|---|---|
videoUrls | array of strings | required | Video URLs or 11-character IDs. Watch, youtu.be, Shorts, embed, live and mobile URLs all work. Duplicates are fetched once. |
languages | array of strings | ["en"] | Preferred caption languages in order. en also matches en-US and en-GB. |
preferManual | boolean | true | Prefer creator-uploaded captions over auto-generated ones in the same language. |
fallbackToAnyLanguage | boolean | true | If none of the preferred languages exists, return the video's default track instead of an error. |
translateTo | string | none | Ask YouTube to auto-translate the chosen track into this language. Best effort, see below. |
includeSegments | boolean | true | Include the timed segments array. Turn off for smaller items. |
includeSrt, includeVtt | boolean | false | Add subtitle-file strings. |
maxConcurrency | integer | 5 | Videos fetched in parallel (1 to 20). |
proxyConfiguration | object | Apify datacenter proxy | Proxy for all requests. |
residentialFallback | boolean | true | If YouTube blocks the datacenter proxy for a video, retry that video through residential proxies. Only used on a detected block. |
Minimal input:
{ "videoUrls": ["https://www.youtube.com/watch?v=jNQXAC9IVRw", "dQw4w9WgXcQ"] }
Languages and translation
- The actor returns what the video has. Most videos have an auto-generated track in the spoken language; many also have creator-uploaded tracks in several languages.
availableLanguageslists them all so you can make a second request for another language. languagesis a preference list. With["bs", "hr", "en"]you get Bosnian if it exists, otherwise Croatian, otherwise English, otherwise the default track (or aLANGUAGE_NOT_AVAILABLEitem if you setfallbackToAnyLanguageto false).translateTorequests YouTube's own machine translation of the chosen track. YouTube refuses this for some tracks and rate-limits it aggressively, so treat it as a bonus: when it is refused, you still get the original transcript, plus atranslationErrorfield saying why.translatableTotells you which languages YouTube offers for that video.
Errors
The run does not fail because one video is broken. Each problem video is a normal dataset item with an error code and an errorMessage, so your pipeline can branch on it:
error | Meaning | Charged? |
|---|---|---|
INVALID_INPUT | The string is not a YouTube video URL or ID | no |
VIDEO_UNAVAILABLE | Deleted, private, region-locked or never existed | yes |
NO_CAPTIONS | The video has no caption track at all (live streams, some music videos, captions disabled by the creator) | yes |
LANGUAGE_NOT_AVAILABLE | None of your languages exists and fallback is off; the message lists what the video offers | yes |
LOGIN_REQUIRED | YouTube only serves this video to signed-in users | yes |
BLOCKED | YouTube refused every retrieval path for this video from every proxy tried | no |
FETCH_FAILED | Network or parsing failure on every path | no |
Items with metadata (title, available languages) are filled in even when the transcript itself could not be returned, so LANGUAGE_NOT_AVAILABLE still tells you which languages to ask for.
Pricing
Pay per event: you pay a small fixed amount per video result (successful transcript, or one of the charged error items above) plus Apify platform usage for the run. Our-side failures (BLOCKED, FETCH_FAILED) and invalid inputs are never charged. Set a maximum total charge on the run and the actor stops cleanly when it is reached, with the unprocessed count in the status message.
Typical cost: a run of 100 videos takes about a minute on 512 MB of memory and transfers roughly 0.1 MB per video.
Reliability
YouTube changed caption delivery in 2025 and 2026: the URLs most scrapers use now return HTTP 200 with an empty body unless a proof-of-origin token is attached, and datacenter IP ranges are throttled. This actor:
- fetches captions through three independent paths (two mobile InnerTube clients, then the web player with a minted proof-of-origin token) and moves to the next one on any block or empty response;
- checks every response at body level, so an empty 200 is treated as a failure, never saved as an empty transcript;
- retries a blocked video once through residential proxies when
residentialFallbackis on; - is tested daily with the default input.
source on each item tells you which path produced it, which is useful when reporting an issue.
Limitations
- Live streams in progress have no caption track; the recording usually gets one a few hours after the stream ends.
- Videos with captions disabled by the creator, and most music videos, return
NO_CAPTIONS. - Private, members-only and age-gated videos that require a signed-in account are not supported; the actor never uses cookies.
- Transcript timing comes from YouTube's caption data. Auto-generated captions contain YouTube's own markers such as
[music]or>>speaker changes. - Chapters are parsed from description timestamps; videos whose chapters were set only in YouTube Studio without timestamps in the description return an empty array.
Using it from code
Apify API, Node:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor('serentity/youtube-transcript-scraper').call({videoUrls: ['https://www.youtube.com/watch?v=jNQXAC9IVRw'],languages: ['en'],});const { items } = await client.dataset(run.defaultDatasetId).listItems();console.log(items[0].text);
Python:
from apify_client import ApifyClientclient = ApifyClient("YOUR_TOKEN")run = client.actor("serentity/youtube-transcript-scraper").call(run_input={"videoUrls": ["jNQXAC9IVRw"]})for item in client.dataset(run["defaultDatasetId"]).iterate_items():print(item["title"], item.get("wordCount"), item.get("error"))
The actor also works as a tool through the Apify MCP server: give an agent the list of URLs and it gets back the transcript text and segments in one call.
Legal
Captions are public data that YouTube serves to any viewer. You are responsible for using transcripts in line with YouTube's terms of service and the copyright of the video's creator. The actor does not download video or audio, does not sign in, and does not collect personal data beyond the public channel name and ID shown on the video page.