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
68
Total users
9
Monthly active users
13 days ago
Last modified
Categories
Share
YouTube Transcript Scraper: Captions, Timestamps & Subtitles
Turn any YouTube video into readable text. Give this actor a video link, a playlist link, a channel link, or a search keyword, and it returns each video's transcript as timed segments, a single block of plain text, and ready to use SRT and WebVTT subtitle files, with optional translation into another language. Export the results to JSON, CSV or Excel, or pull them straight into your app through the API.
Why This Scraper?
- Four ways in. Mix direct video links or IDs, playlist links, and channel links (
@handle,/channel/UC...,/c/...,/user/...) in one run, or switch to search mode to find videos by keyword. - Three formats per video. Every record carries the transcript as 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 another language YouTube supports.
- Handles both caption types. Works with human written captions and auto generated ones, and falls back to whatever the video offers when none of your preferred languages are available.
- Time windows on demand. Trim every transcript to a specific second range, and pay only for the trimmed text.
- Built for large pulls. Resume an interrupted playlist or channel crawl, or schedule the actor to run on a recurring basis and get only what's new, updated, or gone since last time.
- Pay only for transcripts you get. Failed, captions disabled, or incremental mode suppressed videos are never billed.
Use Cases
- Content repurposing: turn a video's spoken words into blog posts, show notes, or social captions without watching it end to end.
- Research and note taking: skim long lectures, podcasts, or interviews as searchable text instead of sitting through the video.
- Localization and accessibility: generate subtitle files or translated transcripts for viewers who need them.
- SEO and content indexing: feed video transcripts into your own search tool or content database.
- Media monitoring: track new uploads from a channel or keyword and pull their transcripts automatically on a schedule.
Data You Get
Sample shape: values are illustrative placeholders, not from a live record.
| 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" (the URL or search query that produced this video) |
sourceType | "search" (also "video", "playlist", "channel") |
language | "English" |
languageCode | "en" |
isGenerated | false (auto generated caption when true) |
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 }, ...] |
data | [{ "start": 0.0, "dur": 1.84, "text": "Hello and welcome" }, ...] (same snippets as segments, in the start / dur / text shape other transcript tools use; numeric) |
srt | "1\n00:00:00,000 --> 00:00:01,840\nHello and welcome\n\n..." |
vtt | "WEBVTT\n\n00:00:00.000 --> 00:00:01.840\nHello and welcome\n\n..." |
trimmedStart / trimmedDuration | 0 / 0 (the time window applied to this video; 0 means no trim) |
success | true |
error | null (a short message when a transcript could not be fetched) |
fetchedAt | "2026-07-30T08:13:20Z" (when this row was fetched, ISO 8601 UTC) |
changeType, changedFields, firstSeenAt and lastSeenAt are added only when Incremental mode is on; a normal run's rows keep the shape above without them.
How to Use
- Pick a mode:
url(paste video, playlist, or channel links) orsearch(find videos by keyword). - Fill in the URLs or search terms for that mode, and set your preferred languages.
- Optionally trim the time window, cap how many videos to process, or turn on translation.
- Click Start, then download the dataset as JSON, CSV, or Excel, or read it through the API.
Fetch one video's transcript:
{"mode": "url","videoUrls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],"languages": ["en"]}
Search YouTube by keyword and transcribe the top results:
{"mode": "search","searchQueries": ["langgraph tutorial", "apify actor development"],"maxVideosPerSource": 5,"languages": ["en"]}
Expand a playlist and a channel, capped per source, translated 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"}
Trim every transcript to a specific time window:
{"mode": "url","videoUrls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],"startSec": 30,"durationSec": 30,"languages": ["en"]}
Run it from your code
Python:
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("abotapi/youtube-transcript-scraper").call(run_input={"mode": "url","videoUrls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],"languages": ["en"],})for video in client.dataset(run["defaultDatasetId"]).iterate_items():print(video["videoTitle"], video["charCount"])
JavaScript:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });const run = await client.actor('abotapi/youtube-transcript-scraper').call({mode: 'url',videoUrls: ['https://www.youtube.com/watch?v=dQw4w9WgXcQ'],languages: ['en'],});const { items } = await client.dataset(run.defaultDatasetId).listItems();
Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the Integrations tab.
Resume and recurring updates
- Resume (
resumeFromRunId) continues one interrupted crawl. Paste its run or dataset ID and this run skips every video already collected there, so a large playlist or channel pull picks up where it left off without re fetching, or re billing, the same videos. - Incremental mode (
incrementalMode) is for scheduling this actor on the samevideoUrls/searchQueriessetup again and again (daily, weekly) and getting only what changed. Every video is classifiedNEW,UPDATED,UNCHANGED,REAPPEARED, orEXPIRED. By default onlyNEW,UPDATED, andREAPPEAREDare returned and billed; turn onemitUnchangedoremitExpiredto also get those.stateKeynames or shares one monitoring campaign's saved state. A video isUPDATEDwhen any returned field differs from the last run; the run timestampfetchedAtand anything you list inignoreFieldsForChangesare never compared. EXPIREDis only ever produced inmode: search, and only once a run has scanned every tracked query to its end. Inmode: urlthe tracked set is whatever links you pasted, so a missing one simply wasn't pasted this run, not confirmed gone.
URL shapes
| # | Shape | Example |
|---|---|---|
| 1 | Watch | https://www.youtube.com/watch?v=dQw4w9WgXcQ |
| 2 | youtu.be short link | https://youtu.be/dQw4w9WgXcQ |
| 3 | Mobile watch | https://m.youtube.com/watch?v=dQw4w9WgXcQ |
| 4 | Shorts | https://www.youtube.com/shorts/jNQXAC9IVRw |
| 5 | Live (replay) | https://www.youtube.com/live/IoL51xLkMSY |
| 6 | Embed | https://www.youtube.com/embed/dQw4w9WgXcQ |
| 7 | Watch with list= (treated as that one video) | https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=PLFgquLnL59alCl_2TQvOiD5Vgm1hCaGSI |
| 8 | Playlist | https://www.youtube.com/playlist?list=PLFgquLnL59alCl_2TQvOiD5Vgm1hCaGSI |
| 9 | Channel @handle | https://www.youtube.com/@YouTube |
| 10 | Channel @handle/videos tab | https://www.youtube.com/@YouTube/videos |
| 11 | Channel /channel/UC... | https://www.youtube.com/channel/UCBR8-60-B28hp2BmDPdntcQ |
| 12 | Legacy /user/ | https://www.youtube.com/user/YouTube |
| 13 | Legacy /c/ | https://www.youtube.com/c/YouTubeCreators |
| 14 | Search results page (expanded like a keyword search) | https://www.youtube.com/results?search_query=python+tutorial |
Default sample (behavior change): an untouched URL mode run, or an API call that omits videoUrls, now scrapes a sample of all 14 shapes instead of failing with an empty input. Videos are deduplicated, so the 5 single video shapes on dQw4w9WgXcQ count once. Billing is per 4,000 character transcript unit, not per video, and long videos bill many units each (one 500,000 character tutorial alone is 125 units). To keep an untouched run cheap, while the 14 sample URLs are exactly as shipped, each playlist, channel, or search source is capped at 1 video (the log says so). Measured on a local test run through the Apify proxy: 7 videos, 73 billed units (about $0.16 at the $0.0022 unit price shown in Pricing), 67 of them from the single top "python tutorial" search hit. Edit the list in any way (add, remove, or replace a URL) and the cap lifts, so maxVideosPerSource (default 10) applies and a search or channel source can bill hundreds of units. Set maxVideosPerSource / maxVideos to bound your own run. A URL that is not a recognised shape is skipped with a warning in the log. If an API call sends only videoUrl (or only searchQueries), the platform still fills in the 14-URL sample, so the actor drops the untouched sample: only your videoUrl is scraped, and when only searchQueries is given the run switches to keyword search mode. Your own videoUrls, if you send any, are never dropped.
Send results into your apps (MCP connectors)
Optionally pipe the scraped transcripts 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.
Input Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
mode | string | "url" | "url" (read videoUrls) or "search" (read searchQueries). The other field is ignored. |
videoUrl | string | (none) | Alias for a single URL; added to videoUrls. URL mode only. |
videoUrls | array | sample of 14 URLs (see "URL shapes") | Used when mode = url. Watch, youtu.be, mobile, shorts, live, and embed URLs, playlist URLs, channel URLs (@handle, @handle/videos, /channel/, /c/, /user/), YouTube search results URLs, or 11 character video IDs. Playlists, channels, and search results pages are expanded to their videos. |
searchQueries | array | [] | Used when mode = search. Keywords to search YouTube; top results per query, up to Max videos per source, are fetched. |
startSec | integer | 0 | Trim every transcript to start at this second. 0 keeps from the beginning of each video. |
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 | 100000 | Hard cap on the total number of videos processed across all sources. Up to 100000; 0 (from older saved tasks) is read as 100000. |
languages | array | ["en"] | Preferred transcript language codes in priority order. The first available one is used; if none are available, any transcript YouTube offers is returned. |
translateToLanguage | string | "" | Optional language code to translate the transcript into using YouTube's auto translation. Empty keeps the original language. |
targetLanguage | string | "" | Alias of translateToLanguage; used only when that field is empty. |
preserveFormatting | boolean | false | Keep inline formatting tags (such as italics) in the transcript text instead of stripping them. |
resumeFromRunId | string | (none) | 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. Classifies each video as NEW, UPDATED, UNCHANGED, REAPPEARED, or EXPIRED. |
emitUnchanged | boolean | false | Incremental mode only. Also return (and bill) videos whose transcript has not 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. |
ignoreFieldsForChanges | array | [] | Incremental mode only. Field names (top-level, or dotted for nested, e.g. segments.start) that never count as a change; fetchedAt is always ignored. Ignored fields still appear in the output. |
stateKey | string | (none) | Optional name for this monitoring campaign's saved state. Leave empty to derive one automatically from the setup. |
proxyConfiguration | object | Apify Proxy, RESIDENTIAL | Connection settings. RESIDENTIAL is the default; datacenter groups also fetched transcripts in a small low-volume test (US only), so you may pick one to lower proxy cost and switch back if many videos return errors. |
mcpConnectors | array | (none) | Optional: send a summary of each record to apps you authorized under Integrations. |
notionParentPageUrl | string | (none) | Notion connector only: page under which records are created. |
maxNotifyListings | integer | 50 | Cap on items written to each connector per run. Does not affect the dataset. |
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.","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","vtt": "WEBVTT\n\n00:00:00.000 --> 00:00:02.320\nHello and welcome to this sample transcript.\n","trimmedStart": 0,"trimmedDuration": 0,"success": true,"error": null,"fetchedAt": "2026-07-30T08:13:20Z"}
With incrementalMode on, rows also carry changeType, changedFields, firstSeenAt, and lastSeenAt.
Output views and run summary
The dataset has two views. Overview lists the video, channel, language, character count and success columns. Changes (Incremental mode) adds changeType, changedFields, firstSeenAt and lastSeenAt. On a failed or blocked row (success: false), language, languageCode, isGenerated and translatedTo are null, the text fields (transcript, srt, vtt) are empty strings, segments and data are empty lists, and charCount and segmentCount are 0; error explains what happened. Every run that finds at least one video also saves a run summary under the OUTPUT key of the key-value store (inputs, videos discovered, transcripts fetched, videos failed, billed transcript units, rows pushed, videos per source; searchQueriesResolved counts keyword searches in search mode only).
Plan Requirement
This actor needs a live connection to YouTube for every transcript fetch, through an Apify proxy. The RESIDENTIAL group is selected by default. Datacenter groups also fetched full pages and transcripts in a small, low-volume test from US exits, so you may pick one to lower proxy cost. That test was a few requests on one day: it does not prove datacenter holds at high volume or from other countries, and YouTube is known to block many datacenter IPs. If many videos return errors, switch back to RESIDENTIAL.
On a free Apify plan (no residential proxy) the run logs this notice and continues on a backup connection:
The selected connection is unavailable to this run. Apify free plans do not include residential proxies. Continuing on a backup connection; some pages may still be unavailable. For residential access, upgrade at https://apify.com/pricing or supply your own proxy URLs.
Videos served that way log "Served over the backup connection (the default connection was unavailable)." If no page could be read at all, the run ends as FAILED (never a silent success with zero rows) with one of these messages:
- Proxy refused for this account: "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required." If you supplied your own proxy URLs and every one of them was refused, the run ends with this same message: check that their credentials are valid.
- Residential unavailable and backup failed: "No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested YouTube pages."
- Requests failed: "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search. Please try again later."
Pricing
Pay per event, as registered on the Store page (Bronze tier shown; higher tiers (Silver, Gold, Platinum, Diamond) are cheaper; the Free tier price is in brackets):
| Event | Price | When it is charged |
|---|---|---|
transcript | $0.002 per unit ($0.0022) | One unit per 4,000 characters (rounded up, minimum one unit) of each successfully fetched transcript that is returned in the dataset, measured on the trimmed text |
search-query | $0.0018 ($0.002) | One per keyword search (search mode only) that YouTube answered, even if it returned no videos; a search that errors is not charged |
apify-actor-start | $0.05 ($0.06) | Per run start, per GB of memory (minimum one) |
Failed, captions disabled, blocked, and Incremental-mode suppressed (UNCHANGED) videos are never charged.
FAQ
How much does it cost?
You pay per run, plus a small fee per keyword search YouTube answers (search mode only), plus a length based unit per fetched transcript, roughly one unit per 4,000 characters of transcript text. Failed or captions disabled videos are never billed, and if you trim a transcript with startSec/durationSec you only pay for the trimmed text. The Pricing tab on this actor's Store page shows the current rates.
Is it legal to scrape YouTube transcripts?
This actor reads publicly available captions attached to a video, the same ones YouTube's own player can display. You are responsible for how you use the results: check YouTube's Terms of Service and the copyright rules that apply to the video's spoken content in your jurisdiction before republishing or redistributing a transcript, especially at scale or for commercial use.
What's the difference between Resume and Incremental mode?
Resume (resumeFromRunId) continues one specific interrupted run, so a large playlist or channel pull that got cut off keeps going without re fetching (and re billing) videos it already collected. Incremental mode is for scheduling the same videoUrls/searchQueries setup again and again and getting only what's new or changed since the last run. They solve different problems and are not normally combined.
Why did I get an auto generated transcript instead of the creator's captions?
The actor prefers a human written transcript over an auto generated one whenever both exist in one of your Preferred transcript languages. If the creator did not upload captions in any of your preferred languages, the actor falls back to whatever the video offers, which may be auto generated.
Why did my run fail instead of returning an empty or partial dataset?
The run fails outright when no page could be read at all (see Plan Requirement), when a resumeFromRunId that cannot be resolved to a run or dataset you own, or when you turn on Incremental mode together with resumeFromRunId when that monitoring setup already has saved state from an earlier run. Otherwise, including empty videoUrls/searchQueries, a video with disabled captions, a private or region locked video, or a temporary block from YouTube on some of the videos, still finishes the run and adds a dataset row explaining what happened, with success set to false, so a partial run always gives you every transcript that could be read. The exception is a run in which not one video got an answer from YouTube (for example every video was temporarily blocked): that run ends FAILED with the connection message described under Plan Requirement, though the rows describing each blocked video are still in the dataset.
Can I get only new or changed transcripts on a schedule?
Yes. Schedule the actor from the Schedules tab and turn on Incremental mode. Each run then classifies every video, and only new, updated, and reappeared rows are returned and billed by default.
Can I use it with AI agents or MCP?
Yes. Call it from any Apify integration or MCP client, and use the connector field to send a summary of each transcript to Notion, Linear, or Airtable at the end of each run.
🔗 Want more video data?
Pair this actor with these related scrapers from the same team:
| 📱 Universal Media Extractor Extract videos, audio, and metadata from 1000+ websites including YouTube, TikTok... | 📱 Lemon8 Media Scraper Lightweight Lemon8 scraper for extracting image and video URLs from posts, with optional... |
| 📱 ViewStats Scraper Scrape YouTube channel analytics from ViewStats. Look up channels by handle, URL, or... | 📱 TikTok Profile, Hashtag, Search, Video & Trending Scraper Scrape TikTok without login. Extract profiles, bios, follower stats and videos; search by... |
| 🎬 Twitch ALL IN ONE URL From $1/1K. Scrape Twitch data at scale, including channels, live streams, clips, VODs... | 📱 Tiktok Live Recorder Record TikTok live streams to MP4 with full metadata, all stream quality URLs, and... |
💬 Support
- 🐞 Found a bug or a missing field? Open a ticket on the Issues tab. We usually reply within hours.
- 🛠️ Need another site, extra fields or a private build? Email contact@abotapi.com.
- ⭐ Enjoying it? A quick review on the actor page helps other users find it.