YouTube Transcript API in Python: 10 Lines with requests

youtube_transcript_api._errors.IpBlocked:
Could not retrieve a transcript for the video.
YouTube is blocking requests from your IP.
If that traceback brought you here, welcome. It's the standard failure mode of the popular pip package the moment your code leaves your laptop and lands on AWS, GCP, or any other datacenter IP. This post shows the other route: a hosted YouTube transcript API you call from Python with nothing but requests. By the end you'll have a ten-line script that turns any YouTube URL into transcript text, plus proper branches for rate limits and videos without captions.
Why the pip library dies in production
youtube-transcript-api scrapes YouTube directly from whatever machine runs it. Locally that's fine. Deploy it, and YouTube's bot detection flags your cloud provider's IP range within days, sometimes minutes. I wrote up the gory details in why the YouTube transcript API gets blocked — short version: you end up renting residential proxies to read public captions, which is absurd.
youtube2text.org does the YouTube-facing part server-side. Your code makes one HTTPS call and gets JSON back.
The ten lines
No account needed to try it — there's a demo key endpoint:
import requests
API = "https://youtube2text.org/api/transcribe"
KEY = requests.get("https://youtube2text.org/api/demo-key").json()["apiKey"]
resp = requests.get(API, params={"url": "https://youtu.be/dQw4w9WgXcQ"},
headers={"x-api-key": KEY}, timeout=60)
resp.raise_for_status()
result = resp.json()["result"]
print(result["title"])
print(result["content"][:300])
The url param accepts any YouTube URL shape — watch?v=, youtu.be, shorts, embeds — or a bare 11-character video id. The response gives you videoId, title, pubDate, content, contentSize, and a truncated flag.
One caveat on that demo key: it's shared across everyone and capped at 5 videos per month per IP. Fine for kicking the tires, useless for anything real.
Handling 429 and 404 like an adult
raise_for_status() is fine for a demo, but errors come back as structured JSON with a code you can branch on:
import time
resp = requests.get(API, params={"url": video_url},
headers={"x-api-key": KEY}, timeout=60)
body = resp.json()
if resp.status_code == 200:
transcript = body["result"]["content"]
elif resp.status_code == 429:
time.sleep(body["error"]["retryAfterSeconds"]) # then retry
elif resp.status_code == 404:
if body["error"]["code"] == "TRANSCRIPT_UNAVAILABLE":
transcript = None # video exists, but has no captions — skip it
else:
raise ValueError(f"No such video: {video_url}") # VIDEO_NOT_FOUND
else:
raise RuntimeError(body["error"]["message"])
TRANSCRIPT_UNAVAILABLE deserves a moment of honesty: transcripts come from YouTube's caption tracks (manual or auto-generated), and some videos simply don't have one. No API can conjure text that isn't there — for those you'd need actual speech-to-text, which I compared in Whisper vs YouTube captions. Retrying won't help, so don't.
maxChars and LLM token budgets
If the transcript is headed into a prompt, you care about size. A three-hour podcast transcript will cheerfully eat 100k tokens. The maxChars parameter caps the output — default and maximum are both 150000:
resp = requests.get(API,
params={"url": video_url, "maxChars": 20000},
headers={"x-api-key": KEY}, timeout=60)
At the rough 4-characters-per-token rule, maxChars=20000 lands around 5k tokens. Check result["truncated"] to know whether you got the whole thing. If you're wiring this into a retrieval pipeline instead of a single prompt, the LangChain integration post covers chunking properly.
Getting off the demo key
When 5 shared videos a month stops being enough, sign in with Google at youtube2text.org/app/keys and mint your own key — no phone number, no credit card. The free tier is 5 videos a month per key; paid tiers start at $5.99 for 50. The ten lines above don't change, only the string in the header does.