YouTube Transcript & Subtitles Scraper API avatar

YouTube Transcript & Subtitles Scraper API

Pricing

$2.50 / 1,000 transcripts

Go to Apify Store
YouTube Transcript & Subtitles Scraper API

YouTube Transcript & Subtitles Scraper API

Get YouTube transcripts and captions from videos, playlists and channels as timed segments, plain text, SRT or VTT. Many languages, an auto-generated fallback and video metadata. Each failed video gets a clear error row and the run never hangs. Built for AI and RAG pipelines.

Pricing

$2.50 / 1,000 transcripts

Rating

0.0

(0)

Developer

Michael Lip

Michael Lip

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

12 hours ago

Last modified

Categories

Security

Limited permissions

Share

TubeText YouTube Transcript Scraper

Get YouTube transcripts from videos, playlists and channels as plain text, timestamped segments, SRT or WebVTT, for $0.0025 per saved transcript. Videos that fail cost nothing.

Paste any mix of watch URLs, youtu.be links, Shorts, playlist URLs and @handles, one per line. You get one row per video with the transcript and the video's title, channel, publish date, duration and view count. When a video can't be read (private, age-restricted, no captions, blocked), you get a free row with a clear error code and the run keeps going.

People use it to feed transcripts into RAG pipelines and LLM prompts, to search what a channel has said about a topic, and to make subtitle files.

What you get

One dataset row per video. Every successful row has the full plain text in text. The output format adds segments, SRT or WebVTT on top.

FieldTypeWhat it holds
statusstringok (transcript saved, charged) or error (free)
videoId, urlstringThe 11-character video id and the watch URL
titlestringVideo title
channelName, channelId, channelUrlstringThe channel that uploaded the video
publishDatestringISO 8601 date with YouTube's timezone offset
durationSecondsintegerVideo length in seconds
viewCountintegerViews when the run read the video
isLiveContentbooleantrue for past live streams
categorystringYouTube category, for example Music
languageCode, languageNamestringThe caption track that was used
isGeneratedbooleantrue means YouTube's speech-recognition captions
languageMatchedbooleanfalse when none of your languages existed and the original language came back
availableLanguagesarrayEvery caption track the video has, auto tracks marked (auto)
segmentCount, wordCountintegerNumber of caption lines and words
transcriptEndSecondsnumberWhere the last caption ends
outputFormatstringsegments, text, srt or vtt
textstringThe full transcript as plain text
segmentsarray{ start, duration, text } per caption line, in seconds (segments format)
srt, vttstringA ready subtitle file (srt or vtt format)
sourceobjectWhich input produced the row. Playlist and channel rows add title and position
errorCode, errorMessagestringWhy a video failed (error rows only)

The title, channel, publish date, duration, views and category fields are filled when "Include video metadata" is on, which is the default. The dataset has three views in Console: Transcripts, Errors and Languages. The OUTPUT record in the key-value store holds a run summary with counts per error code.

Example output

Here is a real row from a test run on 2026-11-05 with the prefilled input. The segments list is cut to 2 of its 6 entries. Nothing else is changed.

{
"status": "ok",
"videoId": "jNQXAC9IVRw",
"url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
"title": "Me at the zoo",
"channelName": "jawed",
"channelId": "UC4QobU6STFB0P71PMvOGN5A",
"channelUrl": "https://www.youtube.com/channel/UC4QobU6STFB0P71PMvOGN5A",
"publishDate": "2005-04-23T20:31:52-07:00",
"durationSeconds": 19,
"viewCount": 447802112,
"isLiveContent": false,
"category": "Film & Animation",
"languageCode": "en",
"languageName": "English",
"isGenerated": false,
"languageMatched": true,
"availableLanguages": ["en", "de"],
"segmentCount": 6,
"wordCount": 39,
"transcriptEndSeconds": 18.881,
"outputFormat": "segments",
"text": "All right, so here we are, in front of the elephants the cool thing about these guys is that they have really... really really long trunks and that's cool (baaaaaaaaaaahhh!!) and that's pretty much all there is to say",
"segments": [
{ "start": 1.2, "duration": 2.16, "text": "All right, so here we are, in front of the elephants" },
{ "start": 5.318, "duration": 2.656, "text": "the cool thing about these guys is that they have really..." }
],
"source": { "type": "video", "input": "https://www.youtube.com/watch?v=jNQXAC9IVRw", "id": "jNQXAC9IVRw" },
"errorCode": null,
"errorMessage": null
}

An error row looks like this (shape from the code, values shown for a private video).

{
"status": "error",
"videoId": "<video id>",
"url": "https://www.youtube.com/watch?v=<video id>",
"source": { "type": "video", "input": "<your input line>", "id": "<video id>" },
"errorCode": "VIDEO_PRIVATE",
"errorMessage": "<message from YouTube>"
}

Pricing

Pay per event, one event.

EventWhen it's chargedPrice
Transcript (video)Once per video whose transcript is saved to the dataset$0.0025

Error rows are pushed without an event, so they're free. Listing a playlist or channel is free too. A video from a playlist or channel costs the same as one you paste directly.

Some worked examples.

RunTranscripts savedCost
The prefilled input (2 videos)2$0.005
A playlist of 100 videos, all with captions, max videos per playlist set to 100100$0.25
A channel's latest 50 uploads (max videos per channel set to 50), 40 with captions and 10 NO_CAPTIONS40$0.10
1,000 videos for a RAG index1,000$2.50

To cap your spend, set a maximum cost per run (the max total charge option in Console, or maxTotalChargeUsd in the API). The Actor starts a video only when its $0.0025 still fits. When the budget is used up, the run stops cleanly, and OUTPUT.videosNotStarted tells you how many videos were left. Videos run in input order, so put the ones you care about first.

Quick start in Console

  1. Open the Actor and click Start. The input is prefilled with two short public videos that have English captions.
  2. Our local test of this input took about 12 seconds. It costs $0.005. Open the Transcripts view, or export the dataset as JSON, CSV or Excel.
  3. Replace the URLs with your own videos, playlists or channels and run again.

Use from the API

One call runs the Actor and returns the dataset rows.

curl -X POST \
"https://api.apify.com/v2/acts/zovo-one~tubetext-youtube-transcript-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"urls": [
"https://www.youtube.com/watch?v=jNQXAC9IVRw",
"https://www.youtube.com/@TheCodingTrain"
],
"languages": ["en"],
"outputFormat": "text",
"maxVideosPerSource": 10
}'

The sync endpoint waits up to 5 minutes. For large playlists or channels, start the run with POST /v2/acts/zovo-one~tubetext-youtube-transcript-scraper/runs and read the dataset when it finishes.

Use from Python

$pip install apify-client
from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("zovo-one/tubetext-youtube-transcript-scraper").call(run_input={
"urls": ["https://www.youtube.com/playlist?list=PLRqwX-V7Uu6ZiZxtDDRCi6uhfTH4FilpH"],
"languages": ["en"],
"outputFormat": "segments",
"maxVideosPerSource": 20,
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
if item["status"] == "ok":
print(item["title"], item["wordCount"])
else:
print("skipped", item["videoId"], item["errorCode"])

Use from Node.js

$npm install apify-client
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('zovo-one/tubetext-youtube-transcript-scraper').call({
urls: ['https://www.youtube.com/@TheCodingTrain'],
languages: ['en', 'de'],
outputFormat: 'srt',
maxVideosPerSource: 5,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const item of items) {
if (item.status === 'ok') console.log(item.title, item.srt.length);
}

Use from AI agents through MCP

The Apify MCP server at mcp.apify.com exposes this Actor as a tool to Claude, Cursor, VS Code and other MCP clients. Each client adds servers in its own way. VS Code and Claude Desktop, for example, don't take the JSON below as written. For your client, open the MCP configurator, pick this Actor and copy the setup it gives you. A client that reads an mcpServers block with a url key, such as Cursor, can use this config.

{
"mcpServers": {
"apify": {
"url": "https://mcp.apify.com?tools=zovo-one/tubetext-youtube-transcript-scraper"
}
}
}

On first connect your browser opens so you can sign in to Apify. If your client can't do that, add a "headers": { "Authorization": "Bearer <APIFY_TOKEN>" } entry instead. Agents connected to the default server can also find the Actor with search-actors, run it with call-actor and read the rows with get-dataset-items. Pass maxTotalChargeUsd in the call options to cap what one agent call can spend.

Here's a prompt that works well.

Get the English transcripts of the latest 10 videos on @TheCodingTrain. For each video, give me the title and a two-line summary, and list every video that mentions p5.js.

Transcripts are a good fit for agents. The text field is one clean string per video, and wordCount lets the agent check the size before it puts a transcript into context.

Input reference

Only urls is required. Everything else has a default.

FieldWhat it doesDefault
urlsVideos, playlists or channels, one per lineprefilled with 2 videos
languagesPreferred language codes, in order. en also matches en-US and en-GB["en"]
allowAutoGeneratedUse YouTube's automatic captions when no human-made track exists in your languagestrue
fallbackToAnyLanguageReturn the original-language transcript when none of your languages existstrue
outputFormatsegments, text, srt or vtt. The text field is always includedsegments
includeMetadataAdd title, channel, publish date, duration, views and categorytrue
maxVideosPerSourcePlaylists: the first N videos. Channels: the latest N uploads20 (max 5,000)
maxTotalVideosHard cap for the whole run, after duplicates are removed1000 (max 10,000)
proxyConfigurationProxy settingsApify Proxy, residential
maxConcurrencyVideos read in parallel3 (max 10)
maxRetriesExtra attempts per video after a block, rate limit or network error2 (max 5)

Accepted formats, one per line.

  • Videos. https://www.youtube.com/watch?v=ID, https://youtu.be/ID, /shorts/ID, /embed/ID, /live/ID, m. and music. hosts, or a bare 11-character id. A watch URL with &list= is read as that one video.
  • Playlists. https://www.youtube.com/playlist?list=PL... or a bare playlist id (PL..., UU..., OLAK5uy_...).
  • Channels. https://www.youtube.com/@handle, @handle, /channel/UC..., UC..., /c/name or /user/name. The Actor takes the uploads, newest first. Shorts and past live streams count as uploads.

Limits

The Actor reads public captions only. It never signs in, so private, age-restricted and members-only videos give error rows.

It doesn't transcribe audio. A video with no caption track gives NO_CAPTIONS. It doesn't translate either, so if the video lacks your language, you get the original language (or a free error row with the fallback off). Chapters, comments and descriptions aren't included.

One run takes up to 5,000 input lines and 10,000 videos. A run uses at most 10 languages and 10 parallel videos. YouTube Mix playlists (RD...) are made per viewer and can't be listed, so they give INVALID_INPUT.

YouTube blocks many datacenter IPs. The default proxy is Apify residential. If the proxy you pick can't be set up, the Actor logs a warning and runs without one. A blocked video is retried through a new proxy session, up to maxRetries times. Each request times out after 30 seconds.

A traffic guard protects the run. It counts compressed response bodies, the same number as OUTPUT.responseBodyKBPerVideo. Once 3 videos are done, the run stops if the average passes 225 KB per video. In our tests a video used about 30 to 165 KB on the wire, and the 165 KB one was 6 hours long. So the guard mostly catches large fallback pages from YouTube, but a run made only of very long videos (about 10 hours each) can hit it too.

YouTube's endpoints are undocumented and can change. When they do, you'll see error rows with a code, not a crashed run.

Error codes

errorCodeMeaningRetried
VIDEO_UNAVAILABLEDeleted, removed or wrong idno
VIDEO_PRIVATEPrivate videono
AGE_RESTRICTEDNeeds a signed-in adult accountno
LOGIN_REQUIREDMembers-only or other sign-in contentno
VIDEO_UNPLAYABLEYouTube won't play it, for example region-blocked or still processing. A past live stream whose recording YouTube did not keep gives this code with the message "This live stream recording is not available."no
LIVE_NOT_FINISHEDAn upcoming stream, or one YouTube reports as live or not yet processed. Captions come after the stream endsno
NO_CAPTIONSThe video has no caption tracksno
NO_TRANSCRIPT_IN_LANGUAGENone of your languages exists (fallback off), or only auto captions exist and you turned them off. The message lists the tracks that do existno
EMPTY_TRANSCRIPTThe caption track exists but is emptyno
BLOCKEDYouTube rate-limited or bot-checked the IPyes, new proxy session
PO_TOKEN_REQUIREDYouTube sent an empty caption file (retried) or a track that needs a proof-of-origin token (not retried)empty file only
NETWORK_ERROR, PARSE_ERROR, CONSENT_FAILEDTemporary problemsyes
HTTP_ERRORA request to YouTube got an HTTP erroronly for 408, 429, 5xx or no status code
PLAYLIST_NOT_FOUND, CHANNEL_NOT_FOUNDThe playlist or channel doesn't exist or is privateno
INVALID_INPUTThe line isn't a YouTube video, playlist or channelno

FAQ

Do I pay for videos without captions?

No. Any video that ends as an error row is free, including videos from a playlist or channel. You pay $0.0025 only when a transcript is saved.

Why did I get a transcript in another language?

None of your languages existed, so the Actor returned the video's original language and set languageMatched to false. Turn off "Fall back to the video's own language" to get a free NO_TRANSCRIPT_IN_LANGUAGE row instead.

Can I get only captions written by a person?

Yes. Turn off "Use auto-generated captions". Auto captions have isGenerated: true, have no punctuation and can contain mistakes.

Do Shorts work?

Yes, if the Short has captions. Many Shorts don't, and those give NO_CAPTIONS.

What is the tubetext-state store in my account?

A small key-value store with one record, PLAYER_API_KEY. It's the public web API key that YouTube puts in every watch page, not your key. The Actor saves it so a later run doesn't have to download a 340 KB watch page again. You can delete it any time.

The Actor reads only publicly available captions and never signs in. You're responsible for how you use the data, including YouTube's Terms of Service and the video owner's copyright.

Changelog

See ./CHANGELOG.md.