Answers

yt-dlp says "Sign in to confirm you're not a bot". How do I get YouTube subtitles without cookies?

Short answer

If you only need the words and the numbers, skip the download: the YouTube transcript endpoint returns the subtitles with timestamps for $0.004, and YouTube video detail returns title, description, tags, views and likes for $0.004. No cookies, no signed-in account, nothing runs from your server's IP.

A nightly job runs yt-dlp --write-auto-subs --skip-download on a VPS to collect subtitles for a research corpus. One morning every video fails with Sign in to confirm you're not a bot. The usual fix is --cookies-from-browser, and yt-dlp's own maintainers have discussed warning users that the account behind those cookies can be banned. For a subtitle job, putting a real Google account at risk is a poor trade.

Most subtitle jobs never needed the video file. They need the text and a few facts about the video, and both are available without a download.

What replaces which yt-dlp output

yt-dlp gives you Use instead Price
.vtt / .srt subtitles YouTube video transcript $0.004
title, description, tags, view_count, like_count, duration YouTube video detail $0.004
Both, for 100 videos 200 calls {{cost:youtube.videos.get100+youtube.videos.transcript.get100}}

Detail returns tags as hashtags. Add view=full to get ext.duration_ms.

Write a .vtt file like yt-dlp did

import os, requests

API = "https://api.okdata.online/v1"
H = {"Authorization": f"Bearer {os.environ['OKDATA_API_KEY']}"}

def ts(sec):
    ms = int(round(sec * 1000))
    return f"{ms // 3_600_000:02}:{ms // 60_000 % 60:02}:{ms // 1000 % 60:02}.{ms % 1000:03}"

def save_vtt(video_id, lang="en"):
    body = requests.get(f"{API}/youtube/videos/transcript/get", headers=H,
                        params={"id": video_id, "lang": lang}, timeout=60).json()
    if body["code"] != "ok":
        print(video_id, body["code"])  # not_found = no captions, not charged
        return
    t = body["data"]["item"]
    cues = [f"{ts(s['start'])} --> {ts(s['end'])}\n{s['text']}" for s in t["segments"]]
    with open(f"{video_id}.{t['lang']}.vtt", "w", encoding="utf-8") as f:
        f.write("WEBVTT\n\n" + "\n\n".join(cues) + "\n")

save_vtt("dQw4w9WgXcQ")

What you give up

  • No media. If the job needs the video or audio file itself, this does not replace yt-dlp.
  • One track per call, in your preferred lang when the video has it. Check the returned lang.
  • No flag for manual versus automatic captions.
  • Videos without captions return not_found and cost nothing; there is no speech-to-text.
  • The ID goes in as id; links are not accepted on this endpoint.

When yt-dlp is still the right tool

  • You need the media file. Run yt-dlp from a home connection, or accept the cookie risk with a spare account.
  • Small, occasional batches from a residential IP usually pass the check.
  • Subtitles for your own channel can come from YouTube Studio or the official Data API, since you own the videos.

For whole lists and costs, see the video transcript solution page.

Updated