How to Get YouTube Transcripts as a Developer (4 Methods That Work in 2026)
YouTube transcripts unlock a lot: AI video summarizers, searchable course
databases, RAG over video libraries, dataset generation for fine-tuning,
repurposing videos into articles. But getting transcripts programmatically
is full of sharp edges: disabled captions, rate limits, datacenter IP
blocks, and YouTube’s ever-changing frontend.
This guide walks through every practical method with working code. Method 4
is the managed service I run. Skip ahead if you just want the API call.
The DIY methods below are real and will serve you well for small jobs.
What you’re actually fetching
YouTube stores captions as timed tracks in two flavors:
- Manual captions: uploaded by creators, best accuracy
- Auto-generated captions: YouTube’s speech recognition, most videos
Each track is text plus timing (text/start/duration), servable as SRT,
VTT, or YouTube’s timedtext XML. Everything below ultimately resolves to
that shape.
Method 1: youtube-transcript-api (Python)
The standard open-source library. Start here for scripts and prototypes.
pip install youtube-transcript-api
Enter fullscreen mode Exit fullscreen mode
from youtube_transcript_api import YouTubeTranscriptApi
video_id = "dQw4w9WgXcQ" # the ID from the watch URL
transcript = YouTubeTranscriptApi.get_transcript(video_id)
for entry in transcript:
print(f"[{entry['start']:.2f}s] {entry['text']}")
Enter fullscreen mode Exit fullscreen mode
It returns a list of dicts, one {'text', 'start', 'duration'} per segment.
For other languages, list what’s available first, then fetch or translate:
tl = YouTubeTranscriptApi.list_transcripts(video_id)
for t in tl:
print(t.language_code, "generated:" , t.is_generated)
transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=["id", "en"])
track = tl.find_transcript(["en"])
translated = track.translate("id").fetch() # free, server-side
Enter fullscreen mode Exit fullscreen mode
Handle the caption-less case explicitly instead of catching bare
Exception. You’ll need these codes downstream:
from youtube_transcript_api._errors import TranscriptsDisabled, NoTranscriptFound
try:
transcript = YouTubeTranscriptApi.get_transcript(video_id)
except TranscriptsDisabled:
print("captions disabled: needs audio transcription (Method 3)")
except NoTranscriptFound:
print("no track in the requested languages, try others first")
Enter fullscreen mode Exit fullscreen mode
And format for output with the built-in formatters rather than
hand-rolling SRT timestamps:
from youtube_transcript_api.formatters import TextFormatter, SRTFormatter, WebVTTFormatter
plain = TextFormatter().format_transcript(transcript)
srt = SRTFormatter().format_transcript(transcript)
vtt = WebVTTFormatter().format_transcript(transcript)
Enter fullscreen mode Exit fullscreen mode
Method 2: yt-dlp subtitles (CLI)
If you live in the terminal, yt-dlp pulls subtitle files without a line
of Python:
# auto-generated subs, no media download
yt-dlp --write-auto-sub --skip-download "https://youtube.com/watch?v=VIDEO_ID"
# manual subs as SRT
yt-dlp --write-sub --sub-format srt --skip-download "https://youtube.com/watch?v=VIDEO_ID"
# see what's on offer first
yt-dlp --list-subs "https://youtube.com/watch?v=VIDEO_ID"
Enter fullscreen mode Exit fullscreen mode
Great for one-off downloads. Weak for pipelines: you parse files off disk,
and there’s no fallback when neither track exists.
Method 3: Whisper fallback for caption-less videos
When captions don’t exist, someone has to listen to the audio. The DIY
pipeline is download-then-transcribe:
pip install yt-dlp openai-whisper torch
Enter fullscreen mode Exit fullscreen mode
import whisper
import yt_dlp
with yt_dlp.YoutubeDL({"format": "bestaudio/best", "quiet": True}) as ydl:
info = ydl.extract_info("https://www.youtube.com/watch?v=VIDEO_ID")
audio = info["requested_downloads"][0]["filepath"] # or --extract-audio to wav
model = whisper.load_model("small") # base = fast/rough, small+ = better
result = model.transcribe(audio, word_timestamps=True)
segments = [
{"text": s["text"].strip(), "start": s["start"],
"duration": s["end"] - s["start"]}
for s in result["segments"]
]
Enter fullscreen mode Exit fullscreen mode
Honest costs of this path: a GPU (or very patient CPU), model downloads,
audio storage, and minutes of compute per video. Fine occasionally,
painful at 100 videos/day. This is exactly the step a managed service
should absorb for you.
The problems everyone hits (with fixes)
1. Rate limits and IP blocks. A fresh datacenter IP (AWS/GCP/most VPS)
works for a while, then YouTube starts returning 429s and bot-check pages.
I measured a block after ~100–200 requests in a few hours from a cloud IP.
Fix: add delays between requests for small jobs; for bulk workloads, route
through residential proxies (real ISP IPs), not more cloud boxes. I run my
own bulk jobs on WebShare residential proxies.
They’re cheap per-GB and work out of the box (signing up through that link
supports this project at no extra cost to you).
import time
for vid in video_ids:
try:
transcripts[vid] = YouTubeTranscriptApi.get_transcript(vid)
except Exception as e:
transcripts[vid] = None # record it, don't crash the batch
time.sleep(1)
Enter fullscreen mode Exit fullscreen mode
2. One bad video kills the batch. Private, removed, or caption-less
videos raise, so isolate per-video errors (as above) and keep going.
3. Dirty text. Auto captions come with [Music]/[Applause] artifacts
and shaky casing. Minimal cleanup before embedding:
import re
text = re.sub(r"\[.*?\]", "", " ".join(e["text"] for e in transcript))
text = re.sub(r"\s+", " ", text).strip()
Enter fullscreen mode Exit fullscreen mode
Real-world example: RAG over a playlist
This is where DIY glues everything together: fetch every video, chunk by
segment timestamps so citations link back to the exact moment:
playlist = ["https://www.youtube.com/watch?v=VID1",
"https://www.youtube.com/watch?v=VID2"]
chunks = []
for url in playlist:
vid = url.split("v=")[-1]
try:
t = YouTubeTranscriptApi.get_transcript(vid)
except Exception:
continue # failed video ≠ failed pipeline
chunks += [{"videoId": vid, "start": s["start"], "text": s["text"]}
for s in t]
# chunks → embed → vector DB. Re-run weekly; new videos append.
Enter fullscreen mode Exit fullscreen mode
It works, until a playlist is half caption-less videos, or your cloud IP
gets blocked mid-run, or you need it to run unattended every night.
Method 4: the managed shortcut (my Actor)
I packaged the whole guide above into one API call: caption fetching
with language fallback, free server-side translation, playlist expansion,
per-video error isolation, plus a GPU Whisper fallback for caption-less
videos:
lexiie/youtube-transcript-api.
curl -s -X POST "https://api.apify.com/v2/acts/lexiie~youtube-transcript-api/run-sync-get-dataset-items?token=${APIFY_TOKEN}" \
-H 'Content-Type: application/json' \
-d '{"urls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"], "language": "en"}'
Enter fullscreen mode Exit fullscreen mode
One dataset item per video, same 4-format shape from both engines
(transcriptType: MANUAL | AUTO_GENERATED | ASR_GENERATED), with
playlistId/videoIndex on playlist runs and a structured error.code
on failures. Thin clients so you never touch HTTP:
Python
(pip install requests only) and
Node
(native fetch only), plus an agent skill
and copy-paste examples
for playlists, translation, ASR fallback, and partial failure.
Pricing is pay-per-event, failures free: caption transcripts ~$0.003 each
(tiered down to ~$0.0015), AI fallback a flat ~$0.10 per video regardless
of duration. Per-minute STT APIs bill $0.12–$4.00 per audio hour by comparison.
(Exact rates on the Store Pricing tab.)
Build vs. buy, honestly
DIY (Methods 1–3) if you’re learning, processing a handful of videos a
month, or have requirements no API meets. The Actor if you need
unattended reliability, caption-less videos transcribed instead of
erroring, or playlist-scale runs without babysitting proxies and GPUs.
Either way, transcripts turn YouTube from a watch-only archive into
queryable data. Happy building, and if you ship something on it (RAG,
summarizer, research corpus), tell me what broke. That’s how the roadmap
gets written.
- Actor: https://apify.com/lexiie/youtube-transcript-api
- Code, SDKs, skill, examples: https://github.com/Lexiie/youtube-transcript-api