YouTube Transcript & Subtitle Scraper
Pricing
from $1.00 / 1,000 transcript fetcheds
YouTube Transcript & Subtitle Scraper
Extract transcripts and subtitles from YouTube videos in bulk using video, playlist, channel URLs, or keyword search. Returns timed transcript segments, plain text, SRT, and WebVTT subtitle files, with optional auto-translation to other languages.
Pricing
from $1.00 / 1,000 transcript fetcheds
Rating
0.0
(0)
Developer
Abot API
Maintained by CommunityActor stats
0
Bookmarked
40
Total users
7
Monthly active users
7 days ago
Last modified
Categories
Share
YouTube Transcript & Subtitles Scraper
Pull transcripts and subtitles for YouTube videos in bulk. Give it video URLs, playlist URLs, channel URLs, or just keywords to search YouTube, and it returns each video's transcript as timed segments, as plain text, and as ready-to-use SRT and WebVTT subtitle files. It can also auto-translate transcripts into another language.
Why this scraper
- Four ways in: direct video URLs or IDs, playlist URLs, channel URLs (
@handle,/channel/UC...,/c/...,/user/...), and keyword searches, all in one run. - Every output record carries the transcript three ways: timed
segments, a single plain-texttranscriptfield, andsrtplusvttstrings you can save straight to disk. - Language control: list your preferred languages in priority order, or translate the result into any language YouTube supports.
- Handles both human-written captions and auto-generated ones, and falls back to whatever the video offers when your preferred language is not available.
- Concurrent fetching with automatic connection rotation, so large playlists and channels move quickly.
- Per-source time windows: trim each video's transcript to a specific second range (and only pay for the trimmed text).
- Pay only for what you get: failed, captions-disabled, or incremental-mode-suppressed videos are not billed.
- Resume an interrupted large pull, or schedule the actor to run daily/weekly and get only what's NEW, UPDATED, REAPPEARED, or EXPIRED since last time.
Data you get
Per video (one dataset record). Values below are illustrative placeholders, not from a live video.
| Field | Example |
|---|---|
videoId | EXAMPLE_ID1 |
videoUrl | https://www.youtube.com/watch?v=EXAMPLE_ID1 |
videoTitle | Sample Video Title |
channelName | Sample Channel |
channelUrl | https://www.youtube.com/@SampleChannel |
durationSeconds | 213 |
source | python tutorial |
sourceType | search (one of video, playlist, channel, search) |
language | English |
languageCode | en |
isGenerated | false |
isTranslated | false |
translatedTo | null |
charCount | 5234 |
segmentCount | 142 |
transcript | "Hello and welcome to this sample transcript ..." |
segments | [{ "text": "Hello and welcome", "start": 0.0, "duration": 1.84 }, ...] |
srt | "1\n00:00:00,000 --> 00:00:01,840\nHello and welcome\n\n2\n..." |
vtt | "WEBVTT\n\n00:00:00.000 --> 00:00:01.840\nHello and welcome\n\n..." |
trimmedStart | 0 (start of the time window applied; 0 = no trim) |
trimmedDuration | 0 (length of the time window applied; 0 = no trim) |
success | true |
error | null (a short message when a transcript could not be fetched) |
fetchedAt | 2026-07-30T08:13:20.560058+00:00 (when this row was fetched, ISO 8601 UTC) |
changeType | NEW (incremental mode only — NEW | UPDATED | UNCHANGED | REAPPEARED | EXPIRED) |
changedFields | [] (incremental mode only — field names that changed since the last run, on UPDATED rows) |
firstSeenAt | 2026-07-30T08:13:20Z (incremental mode only) |
lastSeenAt | 2026-07-30T08:13:20Z (incremental mode only) |
changeType/changedFields/firstSeenAt/lastSeenAt are only added when incrementalMode is on; a normal run's rows keep the shape above without them.
How to use
Pick a mode:
mode: "url"readsvideoUrls(paste video, playlist, or channel URLs).mode: "search"readssearchQueries(find videos by keyword).
The other field is ignored. Optionally set startSec and durationSec to trim every video's transcript to the same time window (durationSec: 0 means "until end of video").
Fetch one video's transcript:
{"mode": "url","videoUrls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],"languages": ["en"],"proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }}
Several videos at once (URLs or bare IDs both work):
{"mode": "url","videoUrls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ","https://youtu.be/dQw4w9WgXcQ","dQw4w9WgXcQ"],"languages": ["en", "en-US"],"proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }}
Search YouTube by keyword and transcribe the top results:
{"mode": "search","searchQueries": ["langgraph tutorial", "apify actor development"],"maxVideosPerSource": 5,"languages": ["en"],"proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }}
Expand a playlist and a channel, capped per source, and translate everything to English:
{"mode": "url","videoUrls": ["https://www.youtube.com/playlist?list=PLxxxxxxxxxxxxxxxx","https://www.youtube.com/@SomeChannel"],"maxVideosPerSource": 25,"maxVideos": 100,"languages": ["en"],"translateToLanguage": "en","proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }}
Trim every video's transcript to seconds 30 to 60:
{"mode": "url","videoUrls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],"startSec": 30,"durationSec": 30,"languages": ["en"],"proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }}
The trimmed transcript, segments, srt, and vtt only contain snippets inside the window, and the transcript billing event counts only the trimmed characters.
Resume & recurring updates
Two different, complementary things:
resumeFromRunIdcontinues one interrupted crawl. Paste a previous run ID or dataset ID; this run skips videos it already collected there instead of re-fetching (and re-billing) them.incrementalModeis for scheduling this actor on the samevideoUrls/searchQueriessetup again and again (daily, weekly) and getting only what changed. The actor remembers its own previous run of that setup, keyed automatically from mode + URLs/queries + language settings (or astateKeyyou name yourself). Every video is classifiedNEW,UPDATED,UNCHANGED,REAPPEARED, orEXPIRED; by default onlyNEW/UPDATED/REAPPEAREDare returned (and billed) — turn onemitUnchangedoremitExpiredto also get those.
Schedule daily monitoring of a search, returning only what changed:
{"mode": "search","searchQueries": ["langgraph tutorial"],"maxVideosPerSource": 20,"incrementalMode": true,"languages": ["en"],"proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }}
Continue a large playlist pull that got interrupted:
{"mode": "url","videoUrls": ["https://www.youtube.com/playlist?list=PLxxxxxxxxxxxxxxxx"],"resumeFromRunId": "<previous run ID>","maxVideosPerSource": 500,"languages": ["en"],"proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }}
Notes:
EXPIREDis only ever produced inmode: search, and only once a run genuinely scanned every search query to its end (not capped bymaxVideos, not aresumeFromRunIdrun, not a search that may have returned more results thanmaxVideosPerSourceasked for). Inmode: urlthe tracked set is whatever URLs you pasted, so a video missing this run simply wasn't pasted —EXPIREDcan never fire there.- An
EXPIREDrow is synthesized from the last known data, not re-fetched, so it is never billed thetranscriptevent. - A suppressed
UNCHANGEDvideo still has to be fetched (to compute itscharCountfingerprint) but is never pushed or billed — onlyNEW/UPDATED/REAPPEAREDrows (andUNCHANGED/EXPIREDif you opt in) cost anything.
Input parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
mode | string enum | "url" | One of "url" (read videoUrls) or "search" (read searchQueries). The other field is ignored. |
videoUrls | array of strings | [] | Used when mode = url. Watch URLs, youtu.be URLs, shorts URLs, playlist URLs, channel URLs (@handle, /channel/, /c/, /user/), or 11-character video IDs. Playlists and channels are expanded to their videos. |
searchQueries | array of strings | [] | Used when mode = search. Free-form keywords. Top results per query are fetched. |
startSec | integer | 0 | Trim every transcript to start at this second. 0 keeps from the beginning. |
durationSec | integer | 0 | Trim every transcript to this many seconds from startSec. 0 keeps until the end of each video. |
maxVideosPerSource | integer | 10 | How many videos to take from each playlist, channel, or search query. |
maxVideos | integer | 0 | Hard cap on the total number of videos across all sources. 0 means no overall cap. |
languages | array of strings | ["en"] | Preferred transcript language codes in priority order. The first available one is used; if none are available, any transcript the video offers is returned. |
translateToLanguage | string | "" | Optional language code to translate the transcript into using YouTube's auto-translation. Empty keeps the original language. |
preserveFormatting | boolean | false | Keep inline formatting tags (such as italics) in the transcript text instead of stripping them. |
resumeFromRunId | string | "" | Continue one interrupted run: paste a previous run ID or dataset ID and this run skips videos already collected there. |
incrementalMode | boolean | false | Turn on for recurring monitoring of the same setup. See Resume & recurring updates. |
emitUnchanged | boolean | false | Incremental mode only. Also return (and bill) videos that haven't changed since the last run, marked UNCHANGED. |
emitExpired | boolean | false | Incremental mode only, mode: search only. Also return videos no longer found, marked EXPIRED. Never billed. |
stateKey | string | "" | Incremental mode only. Optional name for this monitoring campaign's saved state. Leave empty to derive one automatically from the setup. |
proxyConfiguration | object | Apify Proxy, RESIDENTIAL | Proxy settings. YouTube blocks most datacenter / cloud IPs from fetching transcripts, so the RESIDENTIAL group is strongly recommended. |
Send results into your apps (MCP connectors)
Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step after the scrape — the Apify dataset is never changed.
What gets written to the connector: a condensed, human-readable summary of each record — not the full JSON. Each item becomes one entry with a title and its key fields flattened to plain text. The complete record always stays in the Apify dataset.
- Authorize a connector once under Apify → Settings → Integrations (Notion, Linear, Airtable, or Apify).
- Select it in the "Pipe results into your apps" input field. (If the picker is empty, you haven't authorized a connector yet.)
- For Notion, also set
notionParentPageUrlto the page where items should be created.
The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.
Output example
Sample shape: values are illustrative placeholders, not from a live video.
{"videoId": "EXAMPLE_ID1","videoUrl": "https://www.youtube.com/watch?v=EXAMPLE_ID1","videoTitle": "Sample Video Title","channelName": "Sample Channel","channelUrl": "https://www.youtube.com/@SampleChannel","durationSeconds": 213,"source": "https://www.youtube.com/watch?v=EXAMPLE_ID1","sourceType": "video","language": "English","languageCode": "en","isGenerated": false,"isTranslated": false,"translatedTo": null,"charCount": 5234,"segmentCount": 142,"transcript": "Hello and welcome to this sample transcript.\nThis is the second line of the transcript.\n...","segments": [{ "text": "Hello and welcome to this sample transcript.", "start": 0.0, "duration": 2.32 },{ "text": "This is the second line of the transcript.", "start": 2.32, "duration": 2.08 }],"srt": "1\n00:00:00,000 --> 00:00:02,320\nHello and welcome to this sample transcript.\n\n2\n00:00:02,320 --> 00:00:04,400\nThis is the second line of the transcript.\n","vtt": "WEBVTT\n\n00:00:00.000 --> 00:00:02.320\nHello and welcome to this sample transcript.\n\n00:00:02.320 --> 00:00:04.400\nThis is the second line of the transcript.\n","success": true,"error": null,"fetchedAt": "2026-07-30T08:13:20.560058+00:00"}
With incrementalMode on, rows also carry changeType, changedFields, firstSeenAt, and lastSeenAt — see Resume & recurring updates.
Billing
You are charged once per run, plus a small amount per keyword search that was successfully resolved (URL inputs are not charged for resolution — and a resolved search is billed even if every video it finds turns out unchanged, since the search itself still ran), plus a length-based unit per fetched transcript. One transcript unit covers roughly 4,000 characters of transcript text, about 1,000 LLM tokens. A short clip is about one unit, a 1-hour podcast around twelve, and a 3-hour video around thirty-five. Videos whose captions are disabled, unavailable, or could not be fetched are not billed. If you trim the transcript with startSec and durationSec, you only pay for the trimmed text. In incrementalMode, a video suppressed as UNCHANGED is not billed either, even though it still had to be fetched to confirm nothing changed — and an EXPIRED row is never billed, since nothing was re-fetched for it. Exact amounts are shown on this actor's Store page.
Plan requirement
Works on any Apify plan, but YouTube blocks most datacenter and cloud IP ranges from fetching transcripts. For reliable results use Apify Proxy with the RESIDENTIAL group, which is available on the Starter plan and higher. On the free plan, expect many videos to return an error; the run will log a notice and add a record explaining the upgrade path.