HomeBlog

YouTube Transcript in Ruby: Net::HTTP and Zero Gems

Ruby script parsing a YouTube transcript response with the standard library

The Ruby reflex is bundle add something before writing a line of code. Resist it for five minutes: fetching a YouTube transcript in Ruby needs nothing outside the standard library, because the API is one HTTPS GET returning JSON, and net/http plus json have been sitting in every Ruby install since forever. By the end of this post you'll have a script that takes any YouTube URL and prints the video title and transcript, with a case statement covering every error the API returns — and an empty Gemfile.

Yes, Net::HTTP's interface is as clunky as its reputation. It's still only twenty lines.

The script

require "net/http"
require "json"
require "uri"

uri = URI("https://youtube2text.org/api/transcribe")
uri.query = URI.encode_www_form(
  url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
)

req = Net::HTTP::Get.new(uri)
req["x-api-key"] = ENV.fetch("YT2TEXT_KEY")

res = Net::HTTP.start(uri.hostname, uri.port,
                      use_ssl: true, read_timeout: 60) do |http|
  http.request(req)
end

data = JSON.parse(res.body)

if res.is_a?(Net::HTTPSuccess)
  transcript = data["result"]
  puts transcript["title"]
  puts "#{transcript['contentSize']} chars, truncated: #{transcript['truncated']}"
  puts transcript["content"][0, 400]
else
  err = data["error"]
  case err["code"]
  when "RATE_LIMIT_EXCEEDED"
    puts "Rate limited. Retry in #{err['retryAfterSeconds']}s."
  when "TRANSCRIPT_UNAVAILABLE"
    puts "No captions on this video. Skip it."
  when "VIDEO_NOT_FOUND", "INVALID_URL"
    puts "Bad video reference: #{err['message']}"
  else
    abort "#{err['code']}: #{err['message']}"
  end
end

The url parameter is forgiving — full watch URLs, youtu.be short links, shorts, or a bare 11-character video id all work. Add maxChars: 20000 to encode_www_form if the transcript is headed into an LLM prompt and you'd rather not pay for a three-hour podcast's worth of tokens; 150000 is the default and the maximum.

If your codebase already has Faraday or httparty, fine, use them — the request is the same three ingredients: the endpoint, a query string, and one header. Speaking of which, req["Authorization"] = "Bearer #{key}" works identically to x-api-key if bearer tokens fit your setup better.

A key in one command

curl https://youtube2text.org/api/demo-key

That returns {"success":true,"apiKey":"yt_..."} with no account, no email, nothing. The catch: it's one shared key for all anonymous users, limited to 5 videos per month per IP. Great for verifying the script works, wrong for anything you'd deploy.

What the transcript actually is

Worth being clear-eyed here: the text comes from YouTube's caption tracks. Manually uploaded captions read beautifully; auto-generated ones occasionally mangle jargon and product names, and there's nothing any API can do about that upstream. English tracks are preferred, with fallback to the first available language. And a video with captions disabled returns TRANSCRIPT_UNAVAILABLE — the case branch above treats it as a skip, not a retry, because retrying a video that has no captions is a very patient way to accomplish nothing.

Where Rubyists take this

The pattern I see most is batch processing: loop over a channel's video ids, fetch each transcript, feed them to something downstream. Students turn lecture videos into study notes; if the downstream is Claude, the free transcripts in Claude walkthrough connects the two without custom glue. The same endpoint from PHP is nearly line-for-line identical — comparison here if your team runs both.

When the shared demo key runs dry, sign in with Google at youtube2text.org/app/keys — no phone, no card. You get your own free key at 5 videos a month, with paid tiers from $5.99 for 50 up to $19.99 unlimited. The script doesn't change; ENV["YT2TEXT_KEY"] does.