A transcript script that works on your laptop and fails on a server is usually being blocked. YouTube blocks most IP addresses that belong to cloud providers, so youtube-transcript-api raises RequestBlocked or IpBlocked once you deploy. The library's README says so and recommends rotating residential proxies.
The official YouTube Data API won't help with other people's videos: downloading a caption track requires permission to edit the video.
So you run proxies yourself or call a hosted API; youtube-transcript-api alternatives compares them. With CleanScript, your server never calls YouTube. What's left is handling the answers.
Three kinds of failure #
| Kind | Codes | What to do |
|---|---|---|
| Try again later | rate_limited (429), source_unavailable (503), processing_failed (503), timeout (504) |
Wait for Retry-After, then retry. |
| Nothing to get | video_unavailable (404), no_captions, language_unavailable, video_too_long (422) |
Retrying won't help. Skip it and log why. |
| Fix the request | invalid_url, unsupported_platform, invalid_request (400), unauthorized (401), insufficient_credits (402) |
Fix the input, the key or the balance. |
No failed request uses credits. Every error has the same shape:
{
"error": {
"code": "video_unavailable",
"message": "This video is private.",
"param": "url",
"details": { "reason": "private" }
},
"request_id": "5f0c7d1e-0b7a-4a39-9c1e-3c1f2b8f6a10"
}Branch on code, never on message. Keep request_id in your logs; if something looks wrong, send it to us and we can find the request. The full list is in Credits & limits.
The SDKs retry for you #
The Python and TypeScript clients retry 429, 503 and network errors twice with backoff, honouring Retry-After. Each transcript and extract call carries an idempotency key, so a retry is never charged twice. When retries run out, you get a typed error:
from cleanscript_ai import CleanScript, CleanScriptError, InsufficientCreditsError, VideoUnavailableError
client = CleanScript(max_retries=4)
def get_text(url):
try:
return client.transcript(url).text
except VideoUnavailableError as error:
print("skip:", url, error.details) # reason: private, removed, age_restricted, live or unavailable
return None
except InsufficientCreditsError as error:
raise SystemExit(f"Out of credits: {error.details['top_up_url']}")
except CleanScriptError as error:
if error.code in {"no_captions", "language_unavailable", "video_too_long"}:
print("skip:", url, error.code)
return None
raiseThe other subclasses are AuthenticationError, RateLimitError and ConnectionFailedError; everything else is a CleanScriptError with a code.
Production checklist #
- Keep the key in a secret store and read it from
CLEANSCRIPT_API_KEY. - Run a few requests at a time. Your limit per minute is in
GET /v1/account, which is free. - Log
request_idand theX-Credits-Remainingheader with each job. - Retry only the first kind of failure. Record the second and move on.
- Stop the queue on
insufficient_creditsinstead of failing every item. - The same request within 6 hours is free, so re-running a half-finished batch costs only what's new.
No-code tools follow the same rules: see transcripts in n8n, Make and Zapier.
Sources
Spot something wrong? Tell us.