HomeBlog

YouTube Transcript in Golang: net/http and Three Structs

Go program decoding a YouTube transcript API response into structs

Here's the entire program. Explanation after.

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"time"
)

type Transcript struct {
	VideoID     string `json:"videoId"`
	Title       string `json:"title"`
	PubDate     string `json:"pubDate"`
	Content     string `json:"content"`
	ContentSize int    `json:"contentSize"`
	Truncated   bool   `json:"truncated"`
}

type APIError struct {
	Code              string `json:"code"`
	Message           string `json:"message"`
	Status            int    `json:"status"`
	RetryAfterSeconds int    `json:"retryAfterSeconds"`
}

type envelope struct {
	Result *Transcript `json:"result"`
	Error  *APIError   `json:"error"`
}

func main() {
	q := url.Values{}
	q.Set("url", "https://www.youtube.com/watch?v=dQw4w9WgXcQ")

	req, err := http.NewRequest(http.MethodGet,
		"https://youtube2text.org/api/transcribe?"+q.Encode(), nil)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	req.Header.Set("x-api-key", os.Getenv("YT2TEXT_KEY"))

	client := &http.Client{Timeout: 60 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	defer resp.Body.Close()

	var env envelope
	if err := json.NewDecoder(resp.Body).Decode(&env); err != nil {
		fmt.Fprintln(os.Stderr, "decode:", err)
		os.Exit(1)
	}

	if env.Error != nil {
		fmt.Fprintf(os.Stderr, "%s: %s\n", env.Error.Code, env.Error.Message)
		os.Exit(1)
	}

	fmt.Println(env.Result.Title)
	fmt.Printf("%d chars, truncated=%v\n",
		env.Result.ContentSize, env.Result.Truncated)
}

go run main.go, done. No third-party modules, which is how getting a YouTube transcript in Golang should look — the API is plain HTTPS returning JSON, so net/http and encoding/json cover everything. There's no official Go library for this, and I'd argue that's a feature.

The envelope trick

Success responses put data under result; failures put a structured object under error. Decoding both into one struct with two pointer fields means a single Decode call handles every response — whichever pointer is non-nil is your answer. No re-reading the body, no sniffing Content-Type, and env.Error.Status matches the HTTP status if you want to double-check.

Codes worth a switch statement

APIError.Code is stable, so branch on it rather than on message strings:

switch env.Error.Code {
case "RATE_LIMIT_EXCEEDED":
	wait := time.Duration(env.Error.RetryAfterSeconds) * time.Second
	time.Sleep(wait) // then retry the request
case "TRANSCRIPT_UNAVAILABLE":
	// video exists but has no caption track — skip permanently
case "VIDEO_NOT_FOUND", "INVALID_URL":
	// bad input; log and move on
default:
	// UNAUTHORIZED, VALIDATION_ERROR, YOUTUBE_ERROR
}

TRANSCRIPT_UNAVAILABLE is the one people mishandle. Transcripts come from YouTube's caption tracks — manual or auto-generated, English preferred, first available language otherwise. If a video has captions turned off, there is no transcript to fetch, and retrying with exponential backoff will not make captions grow. Treat it as a permanent skip in batch jobs, or your worker queue fills with zombies.

Parameters and keys

Two query parameters total. url takes any YouTube URL shape or a bare 11-character id. maxChars caps output length (150000 is default and max) — useful when the transcript feeds an LLM and you're paying per token, which is most Go use cases I hear about, usually RAG over a whole channel.

For a key without any signup, curl https://youtube2text.org/api/demo-key returns a working one — shared across all anonymous users at 5 videos per month per IP, so it's strictly for trying things out. If you're weighing this against shelling out to yt-dlp for subtitle files, I wrote a comparison of yt-dlp subtitles vs the API; and if part of your stack is Node, the same call in JavaScript is even shorter.

For real usage, sign in with Google at youtube2text.org/app/keys — no card, no phone, free tier of 5 videos a month, paid tiers from $5.99 for 50 up to $19.99 unlimited. The Go code above doesn't change; only the env var does.