YouTube Transcript Scraper — captions to Markdown
Pricing
from $2.00 / 1,000 transcript fetcheds
YouTube Transcript Scraper — captions to Markdown
Get YouTube video transcripts as clean Markdown and timed segments. Accepts video, channel and playlist URLs, picks your preferred caption language with automatic fallback, and returns title, channel, publish date, duration, word count and a per-video status. Failed videos are never charged.
Pricing
from $2.00 / 1,000 transcript fetcheds
Rating
0.0
(0)
Developer
Yosuke Sakaguchi
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
10 days ago
Last modified
Categories
Share
Turn YouTube videos into clean, LLM-ready transcripts. Pass video URLs, channel URLs or playlist URLs and get one record per video with the caption text as paragraph-formatted Markdown, the raw timed segments, title, channel, publish date, duration, the caption language that was used, word count and a per-video status. Built for RAG pipelines, summarizers, research agents and anyone who needs the words from a video without watching it.
What you get
Each video produces exactly one dataset record:
| Field | Type | Description |
|---|---|---|
videoId | string | null | 11-character YouTube video ID. null only when an input item could not be parsed or a channel/playlist could not be expanded |
url | string | Canonical https://www.youtube.com/watch?v=... URL |
source | string | The input item this record came from (useful when a channel or playlist was expanded) |
title | string | null | Video title |
channelName | string | null | Channel display name |
channelId | string | null | Channel ID (UC...) |
publishedAt | string | null | Publish date, ISO 8601 (UTC) |
durationSec | integer | null | Video length in seconds |
language | string | null | Language code of the caption track that was used, e.g. en, en-US, ja |
isAutoGenerated | boolean | null | true when the track is YouTube's automatic speech recognition |
availableLanguages | string[] | All caption tracks the video offered, e.g. ["en", "en (auto)", "ja"] |
transcript | string | null | Paragraph-formatted transcript (Markdown). null when outputFormat is segments |
segments | { start, dur, text }[] | Timed caption segments in seconds. Empty when outputFormat is markdown |
wordCount | integer | Words in the transcript (whitespace-separated tokens; Japanese, Chinese and Korean characters are counted individually) |
fetchedAt | string | ISO 8601 timestamp of the fetch |
status | ok | failed | skipped | failed = no captions, unavailable video or fetch error (see error); skipped = spending limit reached |
error | string | Present only when status is not ok |
The run always finishes as SUCCEEDED, even when some videos have no captions or do not exist. Check status per record instead of the run status, so one private video never breaks your pipeline.
Input
| Field | Type | Default | Description |
|---|---|---|---|
videoUrls | string[] | required | Video URLs (watch?v=, youtu.be/, /shorts/, /embed/, /live/), bare 11-character video IDs, channel URLs (/@handle, /channel/UC..., /c/..., /user/...) or playlist URLs (playlist?list=...). Mixed lists are fine |
languages | string[] | ["en"] | Preferred caption languages in order. See Language fallback below |
outputFormat | markdown | segments | both | both | Which transcript fields to include |
includeTimestamps | boolean | false | Prefix each paragraph of transcript with its start time, e.g. [12:34] |
maxVideosPerChannel | integer | 50 | Upper bound on videos taken from each channel or playlist (1–1000). Channels are expanded newest first |
maxConcurrency | integer | 5 | Videos processed in parallel (1–10) |
proxyConfiguration | object | Apify residential proxy | YouTube blocks most datacenter IP ranges, so residential proxies are the default. You can switch to datacenter or custom proxies, but expect more failed records |
Example input:
{"videoUrls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ","https://www.youtube.com/@apify","https://www.youtube.com/playlist?list=PLxxxxxxxxxxxxxxxxxx"],"languages": ["en", "ja"],"outputFormat": "both","includeTimestamps": true,"maxVideosPerChannel": 20}
Output example
{"videoId": "jNQXAC9IVRw","url": "https://www.youtube.com/watch?v=jNQXAC9IVRw","source": "https://www.youtube.com/watch?v=jNQXAC9IVRw","title": "Me at the zoo","channelName": "jawed","channelId": "UC4QobU6STFB0P71PMvOGN5A","publishedAt": "2005-04-24T03:31:52.000Z","durationSec": 19,"language": "en","isAutoGenerated": false,"availableLanguages": ["en", "de"],"transcript": "[00:01] 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, "dur": 2.16, "text": "All right, so here we are, in front of the elephants" },{ "start": 5.318, "dur": 2.656, "text": "the cool thing about these guys is that they have really..." }],"wordCount": 39,"fetchedAt": "2026-10-15T05:12:40.118Z","status": "ok"}
A video without captions looks like this and is not charged:
{"videoId": "a9LDPn-MO4I","url": "https://www.youtube.com/watch?v=a9LDPn-MO4I","source": "https://www.youtube.com/watch?v=a9LDPn-MO4I","status": "failed","error": "No caption tracks available for this video","title": null,"transcript": null,"segments": [],"wordCount": 0,"fetchedAt": "2026-10-15T05:12:40.118Z"}
Other typical error values: Video not available (ERROR): This video is unavailable, Video not available (LOGIN_REQUIRED): This video may be inappropriate for some users. (age-restricted), Playlist or channel unavailable: The playlist does not exist., Not a YouTube URL.
Language fallback
For every video the Actor looks at the caption tracks YouTube offers and picks one in this order:
- For each language in
languages, in order: a manually created track in that language, then an auto-generated track in that language.enalso matches regional variants such asen-USoren-GB; an exact match wins over a variant. - If none of the preferred languages exists: the first manual track in any language.
- Finally: the first auto-generated track in any language.
The chosen track is reported in language and isAutoGenerated, and every track that was on offer is listed in availableLanguages, so you can decide whether the result is good enough for your use case. YouTube sometimes exposes auto-generated tracks in many languages for the same video and sometimes only in the original language; the list can vary between requests.
Transcripts are returned as published by YouTube: the Actor does not translate, summarize or correct them. Auto-generated tracks usually contain recognition errors and, for older videos, no punctuation.
Pricing
Pay per event. You are charged only for videos that return status: "ok".
| Event | Price | When |
|---|---|---|
transcript-fetched | $0.002 per video | A caption track was fetched and converted |
Videos without captions, unavailable or private videos, invalid URLs, and the expansion of channels and playlists are free. Example: transcribing a 200-video channel where 180 videos have captions costs $0.36. You can cap spending per run with the Maximum total charge setting in Apify Console or the maxTotalChargeUsd API parameter; when the cap is reached, remaining videos are returned with status: "skipped".
Usage
Apify Console
Open the Actor, paste video, channel or playlist URLs into Video, channel or playlist URLs, click Start, and download the dataset as JSON, CSV or Excel. The Overview tab shows one row per video with status, language and word count.
JavaScript (apify-client)
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor('fre.d/youtube-transcript').call({videoUrls: ['https://www.youtube.com/watch?v=dQw4w9WgXcQ', 'https://www.youtube.com/@apify'],languages: ['en'],outputFormat: 'markdown',maxVideosPerChannel: 10,});const { items } = await client.dataset(run.defaultDatasetId).listItems();for (const video of items) {if (video.status === 'ok') console.log(video.title, video.language, video.wordCount);else console.warn(video.url, video.error);}
Python (apify-client)
import osfrom apify_client import ApifyClientclient = ApifyClient(token=os.environ["APIFY_TOKEN"])run = client.actor("fre.d/youtube-transcript").call(run_input={"videoUrls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],"languages": ["en", "ja"],"includeTimestamps": True,})for video in client.dataset(run["defaultDatasetId"]).iterate_items():if video["status"] == "ok":print(video["title"], video["transcript"][:200])else:print("failed:", video["url"], video["error"])
AI agents via MCP
Add the Apify MCP server to your agent (Claude Desktop, Cursor, LangGraph, etc.) and allow this Actor:
{"mcpServers": {"apify": {"url": "https://mcp.apify.com/?tools=fre.d/youtube-transcript","headers": { "Authorization": "Bearer <APIFY_TOKEN>" }}}}
The agent can then call the tool with {"videoUrls": ["https://www.youtube.com/watch?v=..."]} and read transcript directly from the result. Because videoUrls accepts channels and playlists, an agent can pull a whole series in one tool call.
HTTP API
curl -X POST "https://api.apify.com/v2/acts/fre.d~youtube-transcript/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"videoUrls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"], "outputFormat": "markdown"}'
How it works
- Each input item is classified as a video, channel or playlist. Channels are resolved to their uploads list; playlists and channels are paged through until
maxVideosPerChannelis reached. Duplicate videos across inputs are fetched once. - For each video the Actor asks YouTube's internal player endpoint for the video details and the list of caption tracks, picks a track using the language fallback above, and downloads that track through the same proxy session (caption URLs are bound to the IP that requested them).
- Caption cues are cleaned (HTML entities decoded, whitespace collapsed) and grouped into paragraphs: a new paragraph starts after a pause of more than 2.5 seconds, at a sentence end once a paragraph is longer than 30 seconds, or at 60 seconds / 150 words at the latest. Japanese, Chinese and Korean cues are joined without inserted spaces.
- The publish date is looked up in a second lightweight call; if it fails,
publishedAtisnulland the record is still returned.
Requests go through Apify residential proxies by default because YouTube refuses or empties caption responses for most datacenter IP ranges. Each parallel worker keeps its own proxy session; a session that gets a bot check or an empty caption body is discarded and the video is retried on a fresh one.
Limitations
- Only videos that have captions (manual or auto-generated) can be transcribed. Live streams return
faileduntil the stream has ended and YouTube has processed captions. - Private, deleted, members-only and age-restricted videos return
failed. The Actor never signs in and never uses cookies. - Channel expansion takes videos from the channel's uploads list (newest first), which includes Shorts and past live streams. There is no filter by date or type yet.
- Auto-generated captions reflect YouTube's speech recognition quality. The Actor does not correct, translate or summarize them.
wordCountis approximate for mixed-script text.- This Actor relies on YouTube's undocumented internal endpoints, which change from time to time. If you see a sudden rise in
failedrecords, check the Actor's Issues tab; fixes are usually released within a few days.
Legal and responsible use
This Actor is not affiliated with or endorsed by YouTube or Google and does not use the official YouTube Data API. It retrieves the caption tracks that YouTube serves publicly to viewers.
Captions and transcripts are the copyrighted work of the video owner (or of YouTube for auto-generated tracks under the owner's content). You are responsible for how you store, redistribute or otherwise use the text you retrieve, including compliance with YouTube's Terms of Service and applicable copyright and data-protection law. Fetching a transcript to summarize or analyze a video for yourself is a different matter from republishing it; when in doubt, link to the video instead of redistributing the text.
Support
Open an issue on the Actor's Issues tab in Apify Store, or email the maintainer listed on the Actor page. Feature requests (date filters for channels, translation via YouTube's translated tracks, SRT/VTT output, chunking for embeddings) are welcome.