YouTube Transcript Scraper · Channels & Playlists · $5/1K avatar

YouTube Transcript Scraper · Channels & Playlists · $5/1K

Pricing

from $4.00 / 1,000 transcript delivereds

Go to Apify Store
YouTube Transcript Scraper · Channels & Playlists · $5/1K

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

Michal Patyjewicz

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

4 days ago

Last modified

Categories

Security

Limited permissions

Share

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?

FieldTypeDescription
statusstringok (transcript delivered, charged), limited (free-plan cap reached, metadata only, free), error (free) or, with listOnly, languages (free).
videoId, urlstringYouTube video ID and canonical watch URL.
sourceUrlstring or nullThe channel or playlist the video came from.
title, channelId, channelName, channelUrlstringVideo and channel identity.
durationSec, viewCount, likeCount, publishedAt, categorynumber / stringVideo statistics. null when YouTube hides a value.
description, thumbnailUrl, keywordsstring / arrayDescription, best thumbnail, creator tags.
language, languageName, isGeneratedstring / booleanWhich caption track was delivered and whether it is auto-generated.
availableLanguagesarrayEvery caption track YouTube lists for the video (code, isGenerated).
segmentsarray{ 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, vttstringPlain transcript and subtitle files, when requested.
timestampedTextstringPlain text with a [mm:ss] or [hh:mm:ss] timestamp on every line, when timestampedText is among the output formats.
cleanTextstringTranscript without sound tags and music symbols, split into paragraphs by a blank line, when cleanText is among the output formats.
paragraphsarrayThe same paragraphs as { start, end, text } (seconds), when requested.
chunksarrayThe cleaned transcript in pieces of chunkSize words: { index, start, end, wordCount, text }, when requested.
channelHandle, subscriberCountstring / numberOnly 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.
extrasErrorstringOnly when the extra request for those fields failed: its error code. The transcript is still delivered.
words, wordTimestampsAvailablearray / booleanWith wordTimestamps on: every word as { text, start, end } in seconds, and whether the captions carried word times.
segmentCount, wordCountnumberSize of the transcript.
isLive, isShort, isTranslatedbooleanFlags: stream in progress, Shorts video, translated track (translation is not available yet, so always false).
publishedAtApproxbooleanPresent and true when publishedAt was estimated from a channel listing's relative date.
charged, chargeEventboolean / stringSet 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, messageboolean / stringOn limited rows: the free-plan daily cap was reached; message explains it.
errorCode, errorMessage, retryablestring / booleanWhy a video failed and whether a later run may succeed.

How to scrape YouTube transcripts

  1. Open the Actor and paste one or more YouTube URLs into YouTube URLs, or add channel handles under Channels or playlist links under Playlists.
  2. Optionally set Languages (for example en, pl), the Caption type and the Output formats you want.
  3. Click Start. Each video becomes one dataset row as soon as it is done.
  4. 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.

PlanPrice per transcriptPer 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:

FieldDefaultNotes
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.
maxVideosPerSource501 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.
maxVideosPerSearch501 to 300 per query (YouTube lists about 300 results per query at most).
includeShortstrueAlso 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.
anyLanguagefalseWhen none of your languages exists for a video, take any other caption track instead of LANGUAGE_NOT_FOUND. Your languages still come first.
listOnlyfalseFree language discovery, see below.
captionTypeanyany 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).
timestampStyleautoOnly used with timestampedText: auto ([mm:ss], switching to [hh:mm:ss] at an hour), mmss or hhmmss.
chunkSize, chunkOverlap300, 30Only used with chunks: words per chunk (60 to 5000) and words shared with the next chunk (0 up to half of chunkSize).
includeChannelStatsfalseAdd channelHandle and subscriberCount. One extra request per channel.
includeSegmentEndfalseAdd an end time (seconds) to every item in segments.
wordTimestampsfalseAdd a words field with the start and end of every word. Auto-generated captions only.
includeMetadatatrueTurn off for smaller rows.
includeChaptersfalseAdd chapters (title, start, end in seconds) and chaptersAuto to every transcript row.
storeFilesfalseAlso save the text, srt, vtt and timestampedText outputs as files in the run's key-value store, with links in each row's files.
maxConcurrency51 to 20.
maxItems–Stop after this many videos attempted (ok, limited and error rows all count).
onlyNewVideosfalseFor channels and playlists, skip videos an earlier run already delivered (see Only new videos).
stateStoreNameyoutube-transcript-deliveredNamed key-value store that remembers delivered videos.
resetDeliveredVideosfalseForget the history of the channels and playlists in this run.
proxyConfigurationApify residentialYouTube 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.

FormatWhat you get
cleanTextOne string. Sound tags, ♪/♫ symbols, >> marks and simple HTML tags are removed, and the text is split into paragraphs separated by a blank line.
paragraphsThe same paragraphs as { start, end, text }, with times in seconds.
chunksThe 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.

  • includeChannelStats adds channelHandle and subscriberCount. It makes one request per channel and shares the answer between that channel's videos in the run. With includeMetadata off 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
  • start and end are seconds. end is the next chapter's start; the last chapter ends at the video's duration (null if that is unknown). A video without chapters gives chapters: [].
  • 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 at 0:00, in increasing order. Chapters that YouTube generates itself are not in the description, so they are not found (chaptersAuto is always false).
  • 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 .auto after the language for auto-generated captions (dQw4w9WgXcQ.en.auto.srt). Text is .txt, timestamped text is .timestamps.txt.
  • The inline text, srt, vtt and timestampedText fields 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 files at all if none were). SUMMARY.filesSaved counts the files saved and SUMMARY.fileSaveFailures those that failed; both appear only when storeFiles is 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.
  • storeFiles needs at least one of text, srt, vtt or timestampedText in outputFormats.

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

errorCodeMeaningRetry later?
INVALID_URLNot a YouTube video, channel or playlist link.no
PRIVATE_OR_REMOVEDPrivate, deleted or never existed; also a channel or playlist that lists no videos.no
AGE_RESTRICTEDNeeds a signed-in viewer; the Actor does not sign in.no
MEMBERS_ONLYChannel-membership content.no
LIVE_VIDEOStream in progress or not yet processed; try after it ends.yes
CAPTIONS_NOT_READYYouTube has not finished generating captions (fresh uploads).yes
NO_CAPTIONSThe video has no caption track at all.no
LANGUAGE_NOT_FOUNDNo track matches your languages / captionType; see availableLanguages.no
IP_BLOCKEDYouTube rate-limited the request after retries; usually a proxy choice issue.yes
TIMEOUTThe video took longer than 60 s.yes
YOUTUBE_CHANGEDYouTube changed a response shape. Try again later; if it keeps happening, report it in the Issues tab.yes
UNKNOWNAnything 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 ApifyClient
client = 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, default youtube-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_ONLY or PRIVATE_OR_REMOVED) is remembered too, so it is not looked up again; set resetDeliveredVideos to try those videos again, for example when YouTube has added captions. Videos that gave any other error row (such as LANGUAGE_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 @name and as its UC... 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 stateStoreName for an independent history, or set resetDeliveredVideos to 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. SUMMARY shows skippedAlreadyDelivered.

Tips

  • Cheaper runs. Leave outputFormats at json,text unless you need subtitle files; SRT and VTT roughly double the row size.
  • Faster runs. Raise maxConcurrency to 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 at availableLanguages on that row and re-run with one of those codes, or use *.
  • Scheduling. Combine a channel URL with onlyNewVideos on an Apify Schedule to collect only new transcripts, or with publishedAfter to 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.