YouTube Transcript Bulk Scraper - Channel and Playlist avatar

YouTube Transcript Bulk Scraper - Channel and Playlist

Pricing

from $0.38 / 1,000 transcripts

Go to Apify Store
YouTube Transcript Bulk Scraper - Channel and Playlist

YouTube Transcript Bulk Scraper - Channel and Playlist

Get YouTube transcripts in bulk from video links, a whole channel or a playlist. Each row has the full text, timed cues, language and an auto-generated flag, plus title, channel, duration and views. You pay for transcripts, not for the videos it lists. $0.40 per 1,000 transcripts.

Pricing

from $0.38 / 1,000 transcripts

Rating

0.0

(0)

Developer

Dami's Studio

Dami's Studio

Maintained by Community

Actor stats

0

Bookmarked

143

Total users

135

Monthly active users

4 hours ago

Last modified

Share

Scrape YouTube transcripts in bulk from a list of videos, a whole channel or a playlist: get one row per video with the whole transcript as plain text, the per-cue timings, the language, and the video's own details next to it. Channels and playlists are expanded into their videos for you, and that expansion costs nothing.

It reads the caption tracks YouTube already holds. There is no speech recognition here, so a video that was uploaded without captions has nothing to return and comes back as an uncharged diagnostic row instead. You pay for transcripts, not for the videos a channel turned out to have.

InputVideo links, bare 11-character ids, a channel URL, @handle or UC... id, or a playlist link or id
OutputOne row per transcript: full text, timed cues, language and auto-generated flag, title, description, channel, duration and view count. Upload date and SRT or VTT files when you ask
Ceiling500 entries in, up to 5,000 transcripts out
Account neededNone
Price$0.40 per 1,000 transcripts on the Free plan, a little less on paid plans. The free plan's $5 a month covers about 12,500

🔍 What YouTube Transcript Bulk Scraper does

Mix whatever you have in one list. Watch links, Shorts links, youtu.be links, embed links, bare video ids, a channel URL, an @handle, a playlist link. Channels are read newest first, playlists in their own order, and a video that shows up twice across two sources is fetched once.

You get the transcript two ways on the same row. text is the whole thing as one readable string, already joined up, which is what you want if it is going into a model or a search index. segments keeps every cue with its start, duration and end, which is what you want for jumping to a moment in the video or building captions.

Language is a preference list, not a filter. Put en, es, pt-BR in and the first one the video actually has is used; pt matches pt-BR. A human-written track wins over the machine one unless you say otherwise, because it has punctuation and speaker turns. Whichever track was used is named on the row, and availableLanguages lists everything the video offered.

📋 What data you get from each YouTube transcript

What you getField
The transcript as one string, with its word and character countstext, wordCount, characterCount
Every cue with start, duration and end in seconds, when segments are onsegments, segmentCount
The track used: language, its name, machine-made or notlanguage, languageName, isAutoGenerated
Every track the video offeredavailableLanguages
The video: id, link, title, length, views and thumbnailvideoId, url, title, durationSeconds, viewCount, thumbnailUrl
Its description, and whether it was a live streamdescription, wasLive
When it went up and its category, when you askpublishedAt, category
Ready subtitle files, when you asksrt, vtt
The channel: name, id and linkchannelName, channelId, channelUrl
Which of your entries produced the rowsourceType, sourceUrl, playlistId

▶️ How to scrape YouTube transcripts in bulk

  1. Open YouTube Transcript Bulk Scraper and click Try for free.
  2. Paste links into Videos, channels or playlists, one per line.
  3. Set Maximum transcripts low for the first run, since that is what you pay for.
  4. Put your language codes into Preferred languages in the order you want them tried.
  5. Click Start, then download the dataset as JSON, CSV or Excel, or pull it from the Apify API.

Run it with the input empty and you get one labelled sample row, uncharged, so you can look at the shape before you queue a real list.

💰 How much does it cost to scrape YouTube transcripts in bulk?

$0.40 per 1,000 transcripts on the Free plan. Paid plans pay a little less: $0.395 on Starter, $0.39 on Scale and $0.385 on Business. On the free plan, the $5 Apify gives you each month covers about 12,500 transcripts.

Only a delivered transcript counts. Expanding a channel or a playlist into its videos adds nothing, however many it finds. A video with no captions, a rejected entry, a video that would not open and the sample row are all uncharged rows. A video your caption type or date window leaves out is not charged either, and neither is the upload-date lookup.

A video that appears in two of your sources is fetched once and counted once.

📥 What you give it

{
"videoUrls": [
"https://www.youtube.com/watch?v=aircAruvnKk",
"https://www.youtube.com/@3blue1brown"
],
"maxItems": 10,
"languages": ["en"]
}
FieldDefaultWhat it is
videoUrlsnoneUp to 500 entries, in any of the forms above, mixed freely. Bare channel ids (UC...) and playlist ids (PL...) work too.
maxItemsnone (the form starts at 10)Hard ceiling on transcripts returned by the whole run. This is what you pay for, so keep it small while testing.
maxVideosPerSourcenone (the form starts at 25)How deep to go into each channel or playlist before moving on. Ignored for links that already point at one video.
languagesnone (the form starts at en)Language codes in priority order. Leave it empty to take whatever track the video ships with.
preferAutoGeneratedfalseTurn it on when you specifically want the machine transcript even though a written one exists.
captionTypeanyhuman keeps only human-written tracks, auto only auto-generated ones. A video without that kind gets a free row and is not charged.
publishedAfternoneOnly videos that went up on or after this day, as 2026-09-01 (UTC) or 30 days. A channel stops at its first older video. Skipped videos are not charged. A channel or playlist stops after 200 videos outside the window, and its free DATE_WINDOW_LIMIT row says so. Adds the upload date to each row.
publishedBeforenoneOnly videos that went up before this day, in the same formats, with the same 200-video stop.
includeSegmentstrueKeeps the per-cue array on each row. Turn it off for plain text and much smaller rows.
subtitleFormatnonesrt, vtt or both: the transcript as subtitle-file text on the row. Same price.
includePublishDatefalseAdds publishedAt and category. One more lookup per video, so a little slower, same price.
concurrencynone (the form starts at 6)How many videos are read at once, 1 to 10.
proxyUrlsnoneOptional. Your own servers, as http://user:pass@host:port. Leave it empty otherwise.

📤 What you get back

A real row from a real run. text and segments are cut short here; on the real row text runs to 822 characters and segments has all 13 cues:

{
"ok": true,
"charged": true,
"recordType": "transcript",
"videoId": "6XPlmCDNLNc",
"url": "https://www.youtube.com/watch?v=6XPlmCDNLNc",
"title": "The 64 sugar cubes puzzle",
"channelName": "3Blue1Brown",
"channelId": "UCYO_jab_esuFRV4b17AJtAw",
"channelUrl": "https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw",
"durationSeconds": 55,
"viewCount": 322213,
"thumbnailUrl": "https://i.ytimg.com/vi/6XPlmCDNLNc/sddefault.jpg?sqp=...",
"language": "en",
"languageName": "English",
"isAutoGenerated": false,
"segmentCount": 13,
"wordCount": 160,
"characterCount": 822,
"text": "It's time for a new puzzle of the month. This one comes from a problem on the 2024 British Math Olympiad. ...",
"segments": [
{ "start": 0, "duration": 1.7, "end": 1.7, "text": "It's time for a new puzzle of the month." },
{ "start": 2.08, "duration": 3.56, "end": 5.64, "text": "This one comes from a problem on the 2024 British Math Olympiad." }
],
"availableLanguages": [
{ "languageCode": "en", "languageName": "English", "isAutoGenerated": false },
{ "languageCode": "en", "languageName": "English (auto-generated)", "isAutoGenerated": true }
],
"sourceType": "channel",
"sourceUrl": "https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw",
"playlistId": "UUYO_jab_esuFRV4b17AJtAw",
"scrapedAt": "2026-09-21T01:27:33.286Z"
}
FieldHow to read it
textwordCount and characterCount measure this field.
segmentsPresent only while includeSegments is on.
isAutoGeneratedWhether the track actually used was the machine one. Worth keeping, since punctuation and accuracy differ a lot.
sourceType, sourceUrlvideo, channel or playlist, and which of your entries it was. Useful when you pasted a mixed list.
viewCount, durationSecondsRead off the video itself at the time of the run.
descriptionAs YouTube shows it under the video, up to 5,000 characters.
wasLivetrue for a live stream or its replay.
publishedAtUTC time the video went up. Only there when you asked for the date or set a date window; null when YouTube did not give it.
srt, vttOnly there when you asked. Save the text as a .srt or .vtt file as it is.

🧾 Reading the output

Three kinds of row land in your dataset, and recordType separates them.

RowHow to spot itBilled
A real transcriptrecordType: "transcript", ok: trueyes
The sample rowrecordType: "sample", _sample: trueno
A diagnosticrecordType: "diagnostic", ok: false, an errorCodeno

Filter on recordType == "transcript" and you have your transcripts. The charged flag is written onto the row as it is built, so treat it as the marker that tells a real row from a sample or a diagnostic, not as a receipt for what was billed.

CodeWhat it means
BAD_INPUTThat entry was not a video, channel or playlist link. Only the first 25 rejects get a row.
NOT_FOUNDThe handle, channel or video does not exist any more.
NO_RESULTSNothing to return: a video with no captions or none of the caption type you asked for, a channel with no uploads, a private playlist, a source with nothing in your date window.
AGE_RESTRICTEDYouTube shows that video only to signed-in viewers who confirm their age, so its captions can't be read.
DATE_UNKNOWNYouTube did not give that video's upload date, so it could not be checked against your date window.
DATE_WINDOW_LIMITThat channel or playlist stopped after 200 videos outside your date window. Narrow the window or lower Videos per channel or playlist.
RATE_LIMITED, SERVER_ERRORThat video would not answer this run. Re-running usually clears it.
NOT_SERVEDThis transcript was not served this run. Re-run it later. Until 12 October 2026 this code was called BLOCKED.
NETWORKThe connection failed, or the video would not open for a reason it did not name.
TIME_BUDGETThe run ran out of time before reaching that video. One row per video left over. The run ends as failed only when the time limit stopped it before any transcript came back.
PROXY_INPUT_ADJUSTEDA setting you gave in proxyUrls could not be used and was replaced.
CHARGE_ERRORBilling failed on a row that was already delivered. Tell us if you see it.
UNEXPECTED_ERRORSomething broke that the run did not classify. Send the run ID.

💡 What people use it for

  • Making a video searchable. Keep segments and a hit gives you the second to jump to.
  • Keeping up with a channel. Set Published on or after to 7 days and a weekly run picks up only what is new.

From a channel to the reaction to what it said, in two runs:

  1. Paste a channel here with Videos per channel or playlist at 50 and keep the text column.
  2. Filter the rows on a phrase in text to find the videos that mention it.
  3. Paste their url into the Video URLs box of YouTube Comments Scraper to read how viewers took it.

🚧 What it does not do

  • Captions only. There is no speech recognition. If the uploader published no caption track, no amount of retrying will produce text.
  • No translation. You get the tracks the video already carries. Asking for fr on a video with only English gets you the English track, not a translation.
  • Channels come from the uploads list, newest first. That misses unlisted videos and sometimes misses Shorts. A date window stops a source after 200 videos outside it.
  • A watch link carrying list= is treated as one video, not as the playlist. Paste the playlist link itself if you want the playlist.
  • The 5,000 ceiling is the setting's maximum, not a measured promise. Long videos make big rows: a 3 hour 13 minute podcast came back as a single 160 KB row.
  • Only the first 25 rejected entries get a BAD_INPUT row. Beyond that they are dropped quietly, so check your list if the count looks short.
  • Private videos are reported as a failure to open, not as a reason of their own. Age-restricted videos get their own AGE_RESTRICTED row.
  • It is not an official YouTube tool. Dami's Studio is independent and is not affiliated with or endorsed by YouTube or Google, or by any other company named on this page.

🧭 Which YouTube scraper do you need?

If you wantUse
Transcripts for a whole channel or playlist in one runThis one
What a single video says, as text, SRT and VTTYouTube Transcript Scraper
A channel's uploads as rows, up to 2,000 per runYouTube Channel Scraper
Videos in a playlistYouTube Playlist Scraper
Shorts from a channel or a keyword, with exact view and like countsYouTube Shorts Scraper
Only the new uploads since your last checkYouTube Upload Monitor
The video, its audio or its subtitles as a fileYouTube Downloader Pro

❓ Questions people ask

Do I need a YouTube API key?

No. No key, no OAuth, no quota to manage.

What if the video has no captions?

You get a NO_RESULTS row naming that video and the run moves on. That row is not charged.

Auto-generated or written captions, which do I get?

The written one when the video has it, because it is punctuated. Tick Prefer auto-generated captions to flip that. To get only one kind and skip videos without it, set Caption type.

Can I get the timings?

Yes, that is segments, and it is on by default. Turn it off when you want smaller rows and only the plain text.

Can I call it from code or connect it to an AI assistant?

Yes. The API tab has ready-made code for Python, JavaScript and the command line. For Claude, ChatGPT or another MCP client, connect https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/youtube-transcript-bulk-scraper. Either way the run happens on your Apify account at the same price.

This reads publicly available caption tracks. Transcripts are somebody's work, so what you may do with them afterwards is a copyright question rather than a scraping one. Apify's write-up on the legality of web scraping is a reasonable starting point, and we are not lawyers.

🆘 If something breaks

Open the Issues tab on the actor page. Include the links you used and the run ID. The diagnostic rows in your dataset usually name the reason already.