Guides · YouTube Transcript Scraper · Published July 15, 2026

YouTube captions.download returns 403: the permission wall, explained

A 403 from the YouTube Data API's captions.download endpoint is not a bug or a quota problem: the endpoint only serves requests authorized by the video's owner. What the docs actually require, the OAuth setup that works for your own videos, and what works for everyone else's.

By slvDev · Updated and technically verified July 17, 2026


The short answer: a 403 from captions.download is the endpoint working as designed, not failing. Per Google's documentation, the YouTube Data API serves a caption download only when the request is authorized by the owner of the video (or an account with edit permission on it). An API key is never enough, more scopes won't help, and OAuth as yourselfdoesn't help for someone else's video — the check is about who owns the video, not how you authenticated. If it's your own video, the fix is the right OAuth setup, shown below. If it isn't, there is no fix inside the official API, by design — and the working alternatives are at the end.

What the endpoint actually requires

Two things trip almost everyone, and both are in the docs rather than in your code:

  • Caption endpoints are OAuth-only. Both captions.list and captions.download require an OAuth token with the youtube.force-ssl scope. A plain API key — the thing that works fine for videos.list or search.list — returns 401/403 here no matter what.
  • Download is owner-gated. Even with a valid OAuth token, captions.downloadchecks whether the authorized account owns (or can edit) the video the track belongs to. Google's documented 403 reason for this case is forbidden. Your token being valid, fresh, and correctly scoped changes nothing if the video belongs to someone else.

In other words: the endpoint exists for creators exporting captions from their own channel, and it enforces exactly that. The full background — including why the quota math makes the endpoint impractical at scale even for owners — is in Why the YouTube Data API won't give you transcripts.

Case 1: it's your own video — the OAuth setup that works

For your own uploads, the endpoint works and is the right, officially supported tool. The requests must be authorized by the channel that owns the video:

pip install google-api-python-client google-auth-oauthlib
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build

SCOPES = ["https://www.googleapis.com/auth/youtube.force-ssl"]

# client_secret.json comes from a Google Cloud project with the
# YouTube Data API enabled. Log in AS THE CHANNEL THAT OWNS THE VIDEO.
flow = InstalledAppFlow.from_client_secrets_file("client_secret.json", SCOPES)
creds = flow.run_local_server(port=0)
youtube = build("youtube", "v3", credentials=creds)

tracks = youtube.captions().list(
    part="snippet", videoId="YOUR_VIDEO_ID"
).execute()

caption_id = tracks["items"][0]["id"]
srt = youtube.captions().download(id=caption_id, tfmt="srt").execute()
open("captions.srt", "wb").write(srt)

Checklist when this still returns 403 on your own video: you logged into the OAuth consent screen with a different Google account than the channel owner (the classic one — a brand-account channel needs the brand account, not your personal login), or the scope is missing or narrower than youtube.force-ssl.

Things that don't fix it (so you can stop trying them)

  • Service accounts.The YouTube Data API doesn't support service-account authorization for channel data — there is no way to "grant" a service account ownership of a channel. Server-to-server setups that work elsewhere in Google Cloud dead-end here.
  • Requesting more scopes. The gate is video ownership, not scope breadth. youtube.force-ssl is already the widest relevant scope.
  • Retrying, backoff, new keys, new projects. A permissions 403 is deterministic. Unlike an IP block, it will return the same answer forever.

Case 2: it's not your video — what actually works

For arbitrary public videos the official API has no path — that is the design, not a gap you can configure around. The two working routes, with their honest trade-offs:

  • The open-source library (youtube-transcript-api) reads the public caption data YouTube's own player uses — no OAuth involved. Free and excellent for notebooks and low-volume work from a residential IP. Deployed to a datacenter it hits YouTube's IP blocks; that failure mode and its fixes are covered in the RequestBlocked / IpBlocked guide.
  • A managed pay-per-result actor — our YouTube Transcript Scraper — takes plain video IDs, needs no Google Cloud project, consent screen, or quota, and returns time-coded JSON at $3.00 per 1,000 delivered transcripts. A video that can't deliver (no captions, unplayable) becomes an unbilled success: falserow with a stable code, and the batch keeps going. What it doesn't change: private, members-only, and age-gated videos stay out of reach for every method that doesn't own the video.

How the actor and the captions endpoints divide the territory — ownership, quota, cost, formats — is laid out dimension by dimension in the captions-endpoints comparison.

The 403 decision in one table

SituationThe fix
Your own videoOAuth as the channel owner with youtube.force-ssl (code above)
Someone else's public video, prototype volumeyoutube-transcript-api from a residential IP
Someone else's public video, unattended pipelineA managed pay-per-result actor — no key, no quota, failures typed and $0
Private / members-only / age-gated videoNo method that doesn't own the video

The pattern across both this 403 and the library's IP block is the same: each error marks the boundary of a stage. The official API ends at videos you own; the free library ends at datacenter deployment. Neither is broken — they're telling you which tool the next stage needs.


Deploying this?

Run one input on the YouTube Transcript Scraper. It comes back as either a billed transcript row or an unbilled, typed failure row — the contract this guide builds on.

A one-item first run costs under a cent, on your existing Apify account.