YouTube Transcript Scraper · Channels & Playlists · $5/1K
Pricing
from $4.00 / 1,000 transcript delivereds
YouTube Transcript Scraper · Channels & Playlists · $5/1K
No cookies, no login. Turn YouTube video, channel and playlist URLs into clean, timestamped transcripts (JSON, text, SRT, VTT) with video metadata. Pay per delivered transcript; error rows are free.
Pricing
from $4.00 / 1,000 transcript delivereds
Rating
0.0
(0)
Developer
Michal Patyjewicz
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
4 days ago
Last modified
Categories
Security
Limited permissionsShare
What does YouTube Transcript Scraper do?
YouTube Transcript Scraper turns YouTube video, channel and playlist URLs into clean, timestamped transcripts with video metadata, in bulk. It is a YouTube transcript API alternative for people who need captions as data: JSON segments, plain text, SRT or WebVTT, plus title, channel, duration, views, likes, publish date and keywords, in one row per video.
It works with no cookies and no login. It reads the captions YouTube already publishes for a video (creator-uploaded or auto-generated) and does not download audio or video. You only pay for transcripts that are actually delivered: videos without captions, private or removed videos, live streams and invalid links come back as free error rows with a clear code.
Why use YouTube Transcript Scraper?
- Batch everything. Paste hundreds of URLs, or give it a channel or a playlist and let it expand them, newest first, with a per-source limit and an optional date window.
- Every URL form.
watch?v=,youtu.be, Shorts, embed, mobile,music.youtube.com, live links,@handles, channel IDs, playlist IDs. Duplicates across sources are removed. - Language control. A priority list of languages, a choice between creator-uploaded and auto-generated captions, and the full list of available tracks on every row so you can retry with a better choice.
- Formats for people and for programs. Timestamped JSON segments for your own pipelines, plain text for search and reading, SRT and VTT for subtitle workflows.
- Metadata at no extra cost. Title, channel, duration, views, likes, publish date, description, thumbnail, keywords and category come with every row.
- Honest pricing. One price per delivered transcript, no charge for error rows, a stated free allowance, and a run summary with counts and latency.
- Apify platform extras. Schedule it, call it from the API, chain it into other Actors, export to JSON, CSV, Excel or HTML, and get monitoring and residential proxies without any setup.
What data can YouTube Transcript Scraper extract?
| Field | Type | Description |
|---|---|---|
status | string | ok (transcript delivered, charged), limited (free-plan cap reached, metadata only, free), error (free) or, with listOnly, languages (free). |
videoId, url | string | YouTube video ID and canonical watch URL. |
sourceUrl | string or null | The channel or playlist the video came from. |
title, channelId, channelName, channelUrl | string | Video and channel identity. |
durationSec, viewCount, likeCount, publishedAt, category | number / string | Video statistics. null when YouTube hides a value. |
description, thumbnailUrl, keywords | string / array | Description, best thumbnail, creator tags. |
language, languageName, isGenerated | string / boolean | Which caption track was delivered and whether it is auto-generated. |
availableLanguages | array | Every caption track YouTube lists for the video (code, isGenerated). |
segments | array | { start, dur, text } in seconds, when json is among the output formats. Each item also gets an end (seconds, or null) when includeSegmentEnd is on. |
text, srt, vtt | string | Plain transcript and subtitle files, when requested. |
timestampedText | string | Plain text with a [mm:ss] or [hh:mm:ss] timestamp on every line, when timestampedText is among the output formats. |
cleanText | string | Transcript without sound tags and music symbols, split into paragraphs by a blank line, when cleanText is among the output formats. |
paragraphs | array | The same paragraphs as { start, end, text } (seconds), when requested. |
chunks | array | The cleaned transcript in pieces of chunkSize words: { index, start, end, wordCount, text }, when requested. |
channelHandle, subscriberCount | string / number | Only with includeChannelStats. The subscriber count is rounded the way YouTube shows it (4.55M becomes 4550000); null when not available. See Channel handle and subscribers. |
extrasError | string | Only when the extra request for those fields failed: its error code. The transcript is still delivered. |
words, wordTimestampsAvailable | array / boolean | With wordTimestamps on: every word as { text, start, end } in seconds, and whether the captions carried word times. |
segmentCount, wordCount | number | Size of the transcript. |
isLive, isShort, isTranslated | boolean | Flags: stream in progress, Shorts video, translated track (translation is not available yet, so always false). |
publishedAtApprox | boolean | Present and true when publishedAt was estimated from a channel listing's relative date. |
charged, chargeEvent | boolean / string | Set just before the row is charged, and the event it is charged under. true can still mean the row was delivered free if the charge call failed or the run's cost limit was hit at that moment (rare); see SUMMARY.charged and SUMMARY.chargeFlagMismatches below. |
limitExceeded, limitReason, message | boolean / string | On limited rows: the free-plan daily cap was reached; message explains it. |
errorCode, errorMessage, retryable | string / boolean | Why a video failed and whether a later run may succeed. |
How to scrape YouTube transcripts
- Open the Actor and paste one or more YouTube URLs into YouTube URLs, or add channel handles under Channels or playlist links under Playlists.
- Optionally set Languages (for example
en, pl), the Caption type and the Output formats you want. - Click Start. Each video becomes one dataset row as soon as it is done.
- Download the dataset as JSON, CSV, Excel or HTML, or read it through the API.
For channels and playlists, Max videos per source (default 50) and the Published after / before dates control how far back the Actor goes.
How much does it cost to scrape YouTube transcripts?
The Actor uses pay-per-event pricing with a single event, transcript, charged once per successfully delivered transcript. Metadata is included. Error rows and free-plan "limited" rows are never charged. Apify's standard Actor-start event ($0.00005 per start) applies.
| Plan | Price per transcript | Per 1,000 transcripts |
|---|---|---|
| Free | $0.005 | $5.00 |
| Bronze | $0.005 | $5.00 |
| Silver | $0.0045 | $4.50 |
| Gold | $0.004 | $4.00 |
| Platinum (enterprise) | $0.004 | $4.00 |
| Diamond (enterprise) | $0.004 | $4.00 |
Worked examples on the Free or Bronze plan:
- 10 videos → $0.05
- 1,000 videos → $5.00
- 100,000 videos → $500
Free plan allowance
Users on the Apify free plan get 25 transcripts per day (UTC). Beyond that, rows keep coming with full metadata and status: "limited", so a run never fails because of the cap. The allowance resets at 00:00 UTC. Paid plans have no cap.
You can also set a Max cost per run in the run options; the Actor stops cleanly when it is reached.
Find out which languages a video has
Set listOnly to true and every video gives one row with status: "languages": its title, channel, duration and availableLanguages (code, isGenerated), and nothing else. No transcript is fetched and nothing is charged (charged: false). A video with no captions at all gives an empty availableLanguages, not an error. Private, removed and similar videos give the usual free error rows. languages, captionType and outputFormats are ignored in this mode. For channels and playlists the date filters use the dates shown in the listing (no extra request), so they can be a day or so off for recent videos.
Because it is free, it is capped: at most 500 videos per run, and a daily number of lookups per Apify account (100 on the free plan, 2,000 on paid plans, counted per UTC day). When the daily number is used up the run stops and says so in its status message. Videos already being looked up at that moment finish, so a run can go a few videos over. Use it to check a list or a channel first, then run again with the languages you found.
Input
See the Input tab for all options. The important ones:
| Field | Default | Notes |
|---|---|---|
startUrls | – | Any YouTube URL: video, Shorts, youtu.be, embed, mobile, channel or playlist. |
videoIds | – | Bare 11-character IDs, handy for API calls. |
channelUrls, playlistUrls | – | Expanded to videos, newest first. |
maxVideosPerSource | 50 | 1 to 1,000 per channel or playlist. |
searchQueries | – | Keywords (up to 100). Each is expanded to YouTube's matching videos in relevance order. Regular videos only: no Shorts, live or upcoming streams. Rows carry the youtube.com/results?search_query=… URL as sourceUrl. |
maxVideosPerSearch | 50 | 1 to 300 per query (YouTube lists about 300 results per query at most). |
includeShorts | true | Also take the channel's Shorts tab. |
publishedAfter, publishedBefore | – | YYYY-MM-DD window for expanded sources. |
languages | ["en"] | Priority list of language codes. en also matches en-US; * accepts anything. |
anyLanguage | false | When none of your languages exists for a video, take any other caption track instead of LANGUAGE_NOT_FOUND. Your languages still come first. |
listOnly | false | Free language discovery, see below. |
captionType | any | any prefers creator captions and falls back to auto-generated; manual; auto. |
outputFormats | ["json","text"] | Any of json, text, srt, vtt. Also timestampedText (a [mm:ss] stamp on every line), cleanText, paragraphs and chunks (see Clean text and chunks). |
timestampStyle | auto | Only used with timestampedText: auto ([mm:ss], switching to [hh:mm:ss] at an hour), mmss or hhmmss. |
chunkSize, chunkOverlap | 300, 30 | Only used with chunks: words per chunk (60 to 5000) and words shared with the next chunk (0 up to half of chunkSize). |
includeChannelStats | false | Add channelHandle and subscriberCount. One extra request per channel. |
includeSegmentEnd | false | Add an end time (seconds) to every item in segments. |
wordTimestamps | false | Add a words field with the start and end of every word. Auto-generated captions only. |
includeMetadata | true | Turn off for smaller rows. |
includeChapters | false | Add chapters (title, start, end in seconds) and chaptersAuto to every transcript row. |
storeFiles | false | Also save the text, srt, vtt and timestampedText outputs as files in the run's key-value store, with links in each row's files. |
maxConcurrency | 5 | 1 to 20. |
maxItems | – | Stop after this many videos attempted (ok, limited and error rows all count). |
onlyNewVideos | false | For channels and playlists, skip videos an earlier run already delivered (see Only new videos). |
stateStoreName | youtube-transcript-delivered | Named key-value store that remembers delivered videos. |
resetDeliveredVideos | false | Forget the history of the channels and playlists in this run. |
proxyConfiguration | Apify residential | YouTube blocks datacenter IPs for caption requests; keep residential unless you know better. |
Output
You can download the dataset in various formats such as JSON, HTML, CSV or Excel. A successful row (trimmed):
{"status": "ok","videoId": "dQw4w9WgXcQ","url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ","sourceUrl": null,"title": "Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster)","channelId": "UCuAXFkgsw1L7xaCfnd5JJOw","channelName": "Rick Astley","durationSec": 213,"publishedAt": "2009-10-25T06:57:33.000Z","viewCount": 1831092906,"likeCount": 19492133,"category": "Music","language": "en","languageName": "English","isGenerated": false,"availableLanguages": [{ "code": "en", "isGenerated": false }, { "code": "en", "isGenerated": true }, { "code": "de-DE", "isGenerated": false }],"segments": [{ "start": 18.64, "dur": 3.12, "text": "We're no strangers to love" }],"text": "We're no strangers to love You know the rules and so do I ...","segmentCount": 61,"wordCount": 380,"charged": true,"chargeEvent": "transcript","fetchedAt": "2026-10-27T21:40:12.000Z"}
An error row (never charged):
{"status": "error","videoId": "jfKfPfyJRdk","url": "https://www.youtube.com/watch?v=jfKfPfyJRdk","errorCode": "LIVE_VIDEO","errorMessage": "This live stream recording is not available.","retryable": true,"charged": false,"fetchedAt": "2026-10-27T21:40:12.000Z"}
The run's key-value store also holds a SUMMARY record with counts per status and error code, charged transcripts, rows whose charged flag did not match the real outcome (chargeFlagMismatches), videos skipped by the date window, latency percentiles and free-tier usage.
Timestamps
Every segment already carries start and dur in seconds, so you can build your own timestamps from the json format. text stays plain, with no timestamps at all.
For a readable transcript, add timestampedText to outputFormats. It gives one line per segment, stamped and ready to paste or skim:
[00:01] [♪♪♪][00:18] ♪ We're no strangers to love ♪[00:22] ♪ You know the rules and so do I ♪
Stamps use [mm:ss] by default (timestampStyle: auto), switching to [hh:mm:ss] once the video reaches an hour; set timestampStyle to mmss or hhmmss to fix the width yourself.
Turn on includeSegmentEnd to add an end (seconds) to every item in segments, alongside start and dur. Auto-generated captions often overlap, so end can land after the next segment's start; that is YouTube's own timing, left as given.
Clean text and chunks
Captions often carry sound tags ([Music], (Applause)), music symbols and >> speaker marks, and they come as short lines. Three optional formats tidy that up. None is on by default, so default rows are unchanged.
| Format | What you get |
|---|---|
cleanText | One string. Sound tags, ♪/♫ symbols, >> marks and simple HTML tags are removed, and the text is split into paragraphs separated by a blank line. |
paragraphs | The same paragraphs as { start, end, text }, with times in seconds. |
chunks | The cleaned text cut into pieces of chunkSize words (default 300) that share chunkOverlap words with the next one (default 30): { index, start, end, wordCount, text }. start and end are the times of the first and last word's caption line. Handy for search, summaries and anything with a size limit. |
How it cleans:
- A bracketed group is removed only when every word in it is a known sound word, such as
[Music],(Laughter)or[Applause and cheering]. The list covers English plus common Spanish, German, French, Portuguese, Italian, Dutch and Polish words (music, applause, laughter, coughs, sighs and the like). Anything else stays:[frankincense]is a word the speaker said, and(Ooh, give you up)is a lyric. Tags in other languages that are not on the list stay too, and so does YouTube's[ __ ]mask for a censored word. - A new paragraph starts at a speaker change, after a pause of 2 seconds or more, after a sentence end once a paragraph reaches 70 words, and at the next caption line once it reaches 140 words (auto-generated captions have little punctuation).
- Words are counted by spaces, so for languages written without spaces (such as Japanese or Chinese) chunks are not useful and paragraphs only break at pauses and speaker changes, not at a length.
- A bracketed group longer than 60 characters is always kept.
Rows get bigger: cleanText is about the size of text, and chunks repeats the overlap words, so it adds about chunkOverlap / chunkSize on top (10 percent with the defaults), plus a few bytes per chunk. wordCount and segmentCount still describe the original transcript.
Channel handle and subscribers
channelHandle and subscriberCount are not in the data the transcript comes from, so they need one more request to YouTube. The option is off by default, so default runs make no extra request.
includeChannelStatsaddschannelHandleandsubscriberCount. It makes one request per channel and shares the answer between that channel's videos in the run. WithincludeMetadataoff the channel is not known beforehand, so it makes one request per video instead.
Each extra request is about 140 to 300 KB through the proxy, against about 27 KB for the average transcript. Each channel costs one such request per run, so a large channel adds little; with includeMetadata off it costs one per video.
YouTube rounds large numbers on the page (4.55M subscribers), and the value here is that rounded number, not an exact count. subscriberCount is null when the channel hides it, channelHandle is null for a channel without a handle. If the extra request fails, the row is still delivered with null in those fields and an extrasError code; the transcript is never lost to it.
Chapters
Set includeChapters to true to add the video's chapters to every transcript row:
"chapters": [{ "title": "Intro", "start": 0, "end": 5 },{ "title": "The cool thing", "start": 5, "end": 17 },{ "title": "End", "start": 17, "end": 19 }],"chaptersAuto": false
startandendare seconds.endis the next chapter's start; the last chapter ends at the video's duration (nullif that is unknown). A video without chapters giveschapters: [].- The chapters are read from the timestamp lines of the video description, such as
0:00 Intro. As on YouTube, the list needs at least three entries, the first at0:00, in increasing order. Chapters that YouTube generates itself are not in the description, so they are not found (chaptersAutois alwaysfalse). - There is no extra charge and no extra request: the description is already fetched.
Transcript files
Set storeFiles to true to get the subtitle and text outputs as real files as well. Every requested text, srt, vtt and timestampedText output is saved in the run's default key-value store, and the row gets a files object with one link each (json stays in the row only):
"files": {"text": "https://api.apify.com/v2/key-value-stores/<storeId>/records/dQw4w9WgXcQ.en.txt?signature=...","srt": "https://api.apify.com/v2/key-value-stores/<storeId>/records/dQw4w9WgXcQ.en.srt?signature=..."}
- File names are
<videoId>.<language>.<extension>, with.autoafter the language for auto-generated captions (dQw4w9WgXcQ.en.auto.srt). Text is.txt, timestamped text is.timestamps.txt. - The inline
text,srt,vttandtimestampedTextfields stay in the row too. - The links are the ones the Apify SDK builds for key-value store records; they carry a signature where your account restricts access to storages.
- Files stay for as long as your Apify plan keeps this run's data; copy them elsewhere if you need them longer.
- There is no extra charge. The files use your own storage.
- If a file cannot be saved, the row is still delivered and charged as usual, with links only for the files that were saved (no
filesat all if none were).SUMMARY.filesSavedcounts the files saved andSUMMARY.fileSaveFailuresthose that failed; both appear only whenstoreFilesis on. - Files are written just before the row is. In the rare case that a row then fails to be delivered, its files may stay in the store unlinked.
storeFilesneeds at least one oftext,srt,vttortimestampedTextinoutputFormats.
Word-level timestamps
Turn on wordTimestamps to get a words field: every word of the transcript with its start and end in seconds.
"words": [{ "text": "We're", "start": 18.8, "end": 19.039 },{ "text": "no", "start": 19.039, "end": 19.359 },{ "text": "strangers", "start": 19.359, "end": 19.84 }],"wordTimestampsAvailable": true
YouTube only sends word times for auto-generated captions, and only the start of each word. The Actor works out each end as the start of the next word (or the end of the caption line). For creator-uploaded captions, words is an empty array and wordTimestampsAvailable is false; the row is still delivered and priced as usual. Joined together, the words give the same text as text, spaces aside; YouTube's own punctuation items are attached to the word before them. Rows get larger: about 45 characters per word, so roughly 125 KB for a 20-minute talk. There is no extra charge. If a transcript is ever delivered through the backup caption path, it has line times only: words is empty and wordTimestampsAvailable is false.
Error codes
errorCode | Meaning | Retry later? |
|---|---|---|
INVALID_URL | Not a YouTube video, channel or playlist link. | no |
PRIVATE_OR_REMOVED | Private, deleted or never existed; also a channel or playlist that lists no videos. | no |
AGE_RESTRICTED | Needs a signed-in viewer; the Actor does not sign in. | no |
MEMBERS_ONLY | Channel-membership content. | no |
LIVE_VIDEO | Stream in progress or not yet processed; try after it ends. | yes |
CAPTIONS_NOT_READY | YouTube has not finished generating captions (fresh uploads). | yes |
NO_CAPTIONS | The video has no caption track at all. | no |
LANGUAGE_NOT_FOUND | No track matches your languages / captionType; see availableLanguages. | no |
IP_BLOCKED | YouTube rate-limited the request after retries; usually a proxy choice issue. | yes |
TIMEOUT | The video took longer than 60 s. | yes |
YOUTUBE_CHANGED | YouTube changed a response shape. Try again later; if it keeps happening, report it in the Issues tab. | yes |
UNKNOWN | Anything else; the message has details. | yes |
Using YouTube Transcript Scraper from code
JavaScript
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });const run = await client.actor('gangling_polygon/youtube-transcript-scraper').call({startUrls: [{ url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' }],languages: ['en'],outputFormats: ['json', 'text'],});const { items } = await client.dataset(run.defaultDatasetId).listItems();console.log(items[0].text);
Python
from apify_client import ApifyClientclient = ApifyClient("YOUR_APIFY_TOKEN")run = client.actor("gangling_polygon/youtube-transcript-scraper").call(run_input={"channelUrls": ["@TED"],"maxVideosPerSource": 20,"publishedAfter": "2026-01-01","outputFormats": ["text"],})for item in client.dataset(run["defaultDatasetId"]).iterate_items():print(item["title"], item.get("wordCount"))
curl
curl -X POST "https://api.apify.com/v2/acts/gangling_polygon~youtube-transcript-scraper/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"videoIds": ["dQw4w9WgXcQ"], "outputFormats": ["text"]}'
Use from the API and integrations
The Actor can be started from the Apify API, the Python and JavaScript clients (examples above), an Apify Schedule, a webhook, or an Apify integration such as Make or Zapier. Pass videoIds or startUrls; segments carries start and dur in seconds so you can link back to a moment in the video, and text is the plain transcript.
Only new videos
Set onlyNewVideos to true on a scheduled run of a channel or playlist and it delivers only the videos that earlier runs did not:
- The history lives in a named key-value store in your account (
stateStoreName, defaultyoutube-transcript-delivered), one record per channel or playlist. Named stores are kept until you delete them; a record holds up to 50,000 video ids. - A video counts as delivered once its transcript row is written. A video that gave a permanent error row (
NO_CAPTIONS,AGE_RESTRICTED,MEMBERS_ONLYorPRIVATE_OR_REMOVED) is remembered too, so it is not looked up again; setresetDeliveredVideosto try those videos again, for example when YouTube has added captions. Videos that gave any other error row (such asLANGUAGE_NOT_FOUND, which depends on the languages you ask for, or a temporary failure) or a free-plan limit row are tried again next time. - Skipped videos do not count toward
maxVideosPerSource: with a limit of 50 and 5 new videos you get those 5. - Videos you give directly (URLs or ids) are always delivered.
- The history is kept per source as you wrote it: the same channel entered as
@nameand as itsUC...id has two histories. - A video that two sources of one run both list is delivered once, and counts as delivered for the source that delivered it only.
- Use a different
stateStoreNamefor an independent history, or setresetDeliveredVideosto start a source over. Runs that use the same store at the same time mostly merge their histories when they save, but the store has no locking, so two runs saving in the same instant can still overwrite each other's newest ids; avoid scheduling overlapping runs on one store. - When nothing is new the run ends without rows.
SUMMARYshowsskippedAlreadyDelivered.
Tips
- Cheaper runs. Leave
outputFormatsatjson,textunless you need subtitle files; SRT and VTT roughly double the row size. - Faster runs. Raise
maxConcurrencyto 10 or 20 for large channels. Residential proxy bandwidth is the main cost driver, about 27 KB per transcript on average in the soak runs, more for long videos (a 2-hour video took about 200 KB). - Picking a language. If you get
LANGUAGE_NOT_FOUND, look atavailableLanguageson that row and re-run with one of those codes, or use*. - Scheduling. Combine a channel URL with
onlyNewVideoson an Apify Schedule to collect only new transcripts, or withpublishedAfterto use a date window.
Limitations
- Translation into languages the video does not have is not available yet.
- Live streams are delivered once the recording is processed, not while live.
- Age-restricted and members-only videos are not supported; the Actor never signs in.
FAQ, disclaimers and support
Is scraping YouTube transcripts legal? Captions are public data that YouTube shows to every viewer. Our Actors are ethical and do not extract any private user data, such as email addresses, gender, or location. They only extract what the user has chosen to share publicly. We therefore believe that our Actors, when used for ethical purposes by Apify users, are safe. However, you should be aware that your results could contain personal data. Personal data is protected by the GDPR in the European Union and by other regulations around the world. You should not scrape personal data unless you have a legitimate reason to do so. If you're unsure whether your reason is legitimate, consult your lawyers.
Why did a video come back as IP_BLOCKED? YouTube blocks datacenter IP ranges for caption requests. Keep the default Apify residential proxy; if you switched to your own proxies, switch back.
Does it work on fresh uploads? Auto-generated captions can take minutes to hours to appear. CAPTIONS_NOT_READY is retryable; run again later.
Success rate. From a soak test of build 0.1.7: 64 hourly scheduled runs between 28 and 30 October 2026, each taking the 25 newest videos of one channel at concurrency 10. Of 1,600 videos attempted, 1,599 transcripts were delivered and there was 1 error, a private or removed video (an expected free error row). There were no IP_BLOCKED rows, the average was 1.09 attempts per video, and a typical run's median time per video (p50) was about 5 seconds. This covers one channel over three days; results depend on YouTube and can change.
Found a problem or missing a feature? Use the Issues tab. For programmatic access see the API tab.
This Actor is not affiliated with, endorsed by or sponsored by YouTube or Google.