The short answer: the YouTube Data API technically has caption endpoints, but captions.download requires OAuth authorization from the channel that owns the video. An API key is not enough. That means the official API can export captions from your own uploads, and cannot download transcripts for arbitrary public videos, which is what almost everyone asking this question wants. Below is exactly where the wall is, and what works instead.
What the Data API actually offers for captions
The API surface looks promising at first: two endpoints, both OAuth-gated.
captions.listreturns metadata about a video's caption tracks (language, type, last updated). Google's docs require OAuth scopes even for this call; an API key alone is not enough. And even authorized, it only tells you a transcript exists. It does not give you the text.captions.downloadreturns the actual caption track file. This is the one you want, and it is the one you can't have: per Google's own documentation, the API supports downloading a caption track only if the request is authorized by the owner of (or a party with edit permission on) the video it belongs to. Requests for videos you don't own fail with a permissions error, not a quota error.
So the pattern that seems obvious (enumerate videos, call captions.download on each) only ever works for a channel you can authenticate as. If you are building creator tooling for channel owners, the official API is the right choice and this article ends here for you. For everyone else, it is a dead end by design.
The quota makes it worse, not better
Even in the cases where the official route works, the default quota is 10,000 units per project per day, and caption operations are priced steeply against it: per Google's quota cost table, a captions.list call costs 50 units and a captions.downloadcall costs 200. That is 200 list calls or 50 downloads and your day's quota is gone. Quota increases require an application and review. For anything shaped like "process thousands of videos," the arithmetic fails before the permissions do.
What people try next (and how it goes)
- The unofficial timedtext endpoint.YouTube's player fetches captions from an internal endpoint, and you can imitate it. It works until it changes without notice, demands proof-of-origin tokens, or blocks your IP range. Fine for a weekend script; a maintenance treadmill in production.
- Open-source libraries (like Python's
youtube-transcript-api) wrap that same internal surface with a nicer API. Excellent for prototypes and low volume from residential IPs; use them for that. Deployed to the cloud, they inherit the IP-blocking problem: the library's own README documents that YouTube blocks requests from cloud-provider IPs and recommends rotating residential proxies for production use. Full code and trade-offs in our Python transcript guide. - Headless browsers. Loading the watch page in Playwright and scraping the transcript panel works, at a far higher compute cost per video, and the anti-bot fight comes to you anyway.
- Speech-to-text on the audio. Downloading audio and running Whisper sidesteps captions entirely, at meaningful GPU cost per video, minutes of latency, and its own audio-download fragility. Worth it only when videos have no captions at all.
What works for arbitrary public videos at scale
The remaining option is a managed extractor: a service whose whole job is keeping the caption path working (proxies, retries, format changes) and selling the result per delivered transcript. That is what our YouTube Transcript Scraper is: send up to 50 video IDs, get time-coded JSON back, $3.00 per 1,000 delivered transcripts on your existing Apify account. No Google Cloud project, no OAuth consent screen, no quota application.
The design decision that matters for pipelines is the failure contract: a video with no captions, an unplayable ID, or an upstream stall comes back as a success: false row with a stable machine-readable code (NO_CAPTIONS, PLAYABILITY, DEADLINE_EXCEEDED) in the same dataset, free, without failing the run. One bad video never costs you the batch.
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("apihq/youtube-transcript-scraper").call(
run_input={"videoIds": ["jNQXAC9IVRw", "dQw4w9WgXcQ"]}
)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
if item["success"]:
print(item["video_id"], len(item["transcript"]), "segments")
else:
print(item["video_id"], "->", item["code"]) # not billedThe limits, whatever route you pick
- Every caption-based method, official or not, reads tracks that already exist. A video with no captions returns nothing everywhere; only speech-to-text can fill that gap.
- Auto-generated (ASR) tracks carry YouTube's recognition errors. For most search, RAG, and analysis workloads they are plenty; for legal-grade accuracy they are not.
- Private, members-only, and age-gated videos are out of reach for every method that doesn't own the video.
The decision in one table
| You are… | Use |
|---|---|
| Exporting captions from your own channel | YouTube Data API (captions.download with owner OAuth) |
| Prototyping locally, low volume | youtube-transcript-api, free |
| Running an unattended pipeline on arbitrary public videos | A managed pay-per-result extractor like the YouTube Transcript Scraper |
| Processing videos that have no captions at all | Speech-to-text (Whisper or similar) on the audio |
A fuller dimension-by-dimension version of this decision — with every Data API claim linked to Google's documentation — is in the captions-endpoints comparison.