How to get YouTube transcripts with Python (without API quotas)
The YouTube Data API skips transcripts, and youtube-transcript-api gets IP blocked on cloud servers. A setup that costs 1 credit per video, August 2026.

You want the spoken text of a YouTube video: for a dataset, for search, for subtitles. This turns out to be harder than it should be, for two reasons that have nothing to do with your code.
Why doesn’t the YouTube Data API just give you this?
The official YouTube Data API has a captions resource, but captions.download requires OAuth authentication as the video’s own owner. It was built so channel owners can manage their own caption tracks, not so third parties can fetch transcripts for videos they do not own. For everyone else, there is no quota-metered, official way to pull a transcript through the Data API. That gap is why an entire category of unofficial libraries exists.
Why does youtube-transcript-api break on a server?
youtube-transcript-api works by talking to an internal, unofficial YouTube endpoint, and it runs fine on a laptop. Deploy the same code to AWS, GCP or Azure and it starts throwing RequestBlocked or IpBlocked errors, because YouTube blocks the IP ranges known to belong to cloud providers. This is not a bug in the library: it is documented behaviour, discussed in the project’s own GitHub issues as an ongoing problem through 2026 (issue #593, issue #511). Static proxies get banned after moderate use; only rotating residential proxies hold up, and running that infrastructure yourself is a project in its own right if transcripts are not your product.
So the honest state of play: the official API does not do this, and the free library that fills the gap does not survive contact with a production server.
The version that works
Send the video ID to an API that already handles the IP problem, and get the transcript back as JSON:
import requests
API_KEY = "faf_live_..." # your FastAF key
resp = requests.get(
"https://api.fastaf.co/v1/youtube",
params={"v": "dQw4w9WgXcQ", "language": "en"},
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=60,
)
resp.raise_for_status()
data = resp.json()
full_text = " ".join(segment["text"] for segment in data["transcripts"])
print(full_text[:500])
One request costs 1 credit, regardless of video length. v is the YouTube video ID (the part after v= in the URL), language requests a specific caption track when the video has more than one, and country scopes the request to a region when a video’s availability or default captions differ by market.
What can you build with this?
Datasets for LLM fine-tuning or RAG. Transcripts are clean, timestamped natural language at a fraction of the cost of manual transcription. Pull a channel’s back catalog once, store the text, chunk it however your pipeline expects.
Subtitles for your own video pipeline. If you republish, clip or dub video content, a transcript with timing is the starting point for burned-in captions or a translation pass, without asking a human to transcribe from audio.
Searching inside video content. Product demos, podcasts, lecture series: transcripts let you grep video the way you grep text. Index the transcript, and “which episode mentioned X” becomes a search query instead of a scrubbing session.
A minimal batch job for the first case:
import sqlite3
import time
conn = sqlite3.connect("transcripts.db")
conn.execute("""
CREATE TABLE IF NOT EXISTS transcripts (
video_id TEXT PRIMARY KEY,
text TEXT,
fetched_at INTEGER
)
""")
def fetch_transcript(video_id, language="en"):
resp = requests.get(
"https://api.fastaf.co/v1/youtube",
params={"v": video_id, "language": language},
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=60,
)
resp.raise_for_status()
return " ".join(seg["text"] for seg in resp.json()["transcripts"])
video_ids = ["dQw4w9WgXcQ", "9bZkp7q19f0"] # your list
for vid in video_ids:
text = fetch_transcript(vid)
conn.execute(
"INSERT OR REPLACE INTO transcripts VALUES (?, ?, ?)",
(vid, text, int(time.time())),
)
conn.commit()
A thousand videos processed this way is 1,000 credits: the free tier (500 credits, no card) covers the first 500 for a test, and anything bigger is a rounding error on the Starter pack.
The short version
The official API was not built for this and the free library breaks the moment it leaves your laptop. Fetching by video ID over a normal HTTPS request sidesteps both problems, costs 1 credit per video regardless of length, and gives you timestamped text you can drop straight into a dataset, a subtitle track or a search index. See the docs for the full response shape.