All guides

November 10, 2026 · 4 min read

YouTube Transcripts in GitHub Actions and Scheduled Jobs

In short

youtube-transcript-api and yt-dlp fail in GitHub Actions runners, cron servers, and Docker hosts on DigitalOcean, Hetzner, Render, or Railway, because YouTube blocks datacenter IPs. Call a transcript API from the job instead.

Scheduled jobs that pull new YouTube transcripts, for a newsletter, a search index, or an AI digest, usually run on a GitHub Actions runner, a VPS, or a container platform. All of them use datacenter IPs: GitHub's hosted runners run on Azure, and DigitalOcean, Hetzner, Linode, OVH, Render, Railway, Fly.io, and Heroku are all well-known ranges. YouTube blocks caption requests from them, so the job fails with RequestBlocked, IpBlocked, or a yt-dlp bot check.

A daily workflow that fetches new transcripts

Add TRANSCRIPTYT_KEY as a repository secret. The job lists a channel's latest uploads and saves each new transcript as a Markdown file in the repo.

# .github/workflows/transcripts.yml
on:
  schedule: [{ cron: "0 6 * * *" }]
  workflow_dispatch:
jobs:
  fetch:
    runs-on: ubuntu-latest
    permissions: { contents: write }
    steps:
      - uses: actions/checkout@v4
      - run: python fetch.py
        env:
          TRANSCRIPTYT_KEY: ${{ secrets.TRANSCRIPTYT_KEY }}
      - run: |
          git config user.name github-actions
          git config user.email github-actions@users.noreply.github.com
          git add transcripts && git commit -m "New transcripts" || true
          git push

The script

# fetch.py
import os, pathlib, requests

API = "https://transcript-yt.com/v1"
H = {"Authorization": f"Bearer {os.environ['TRANSCRIPTYT_KEY']}"}
out = pathlib.Path("transcripts"); out.mkdir(exist_ok=True)

videos = requests.get(f"{API}/playlist", params={"url": "https://www.youtube.com/@mkbhd", "limit": 10},
                      headers=H, timeout=60).json()["videos"]
for v in videos:
    path = out / f"{v['videoId']}.md"
    if path.exists():
        continue
    r = requests.get(f"{API}/transcript", params={"url": v["videoId"], "format": "md"},
                     headers=H, timeout=300)
    if r.ok:
        path.write_text(r.text)

Same job on a VPS or container

The script runs unchanged from cron, a Docker container, or a Render or Railway cron job, since only transcript-yt.com is contacted. Without code, the n8n, Make, and Zapier guides build the same schedule.

Common questions

Why does youtube-transcript-api fail in GitHub Actions?

GitHub-hosted runners use Azure datacenter IPs, which YouTube blocks for caption requests. A self-hosted runner on a home connection works, but ties the job to that machine.

Does a VPS on Hetzner or DigitalOcean work?

Usually not for long. VPS providers' IP ranges are known datacenter ranges, and YouTube blocks or rate-limits them, sometimes from the first request.

How do I get new videos from a channel on a schedule?

Call /v1/playlist with the channel URL to list its latest uploads (free on paid plans), keep the IDs you've already processed, and fetch transcripts only for new ones.

How do I store the API key in GitHub Actions?

Add it as a repository secret and pass it to the step as an environment variable: env: TRANSCRIPTYT_KEY: ${{ secrets.TRANSCRIPTYT_KEY }}.

Try the YouTube Transcript API

Get transcripts as JSON, text, SRT, or VTT. 100 free credits, no card required.

More guides