Multilingual YouTube Subtitle Translator API
David Boulen · 7/20/2026 · 9 min read
Multilingual YouTube Subtitle Translator API
Most YouTube videos exist in one language, but your users speak dozens. YouTube's own auto-translation is inconsistent and often unavailable for less common language pairs, so building your own translation pipeline is the practical path for any localization team shipping at scale.
This guide walks through the full pipeline: fetch a timed transcript as JSON, detect the source language, pipe segments into a translation model, and export to SRT or VTT. You get copy-paste examples in Python and Node.js, plus the gotchas that will save you a day of debugging.
Why YouTube's Native Translation Falls Short
YouTube offers auto-generated captions for many videos and sometimes auto-translates them into other languages. In practice, availability is inconsistent. Popular videos in major languages get reasonable coverage, but niche content, non-English originals, and videos with heavy background noise often have no captions at all, or only a rough auto-generated track without punctuation.
Even when translations exist, you cannot reliably access them through the YouTube Data API v3 without navigating the captions resource, which requires OAuth and the video owner's permission. For content you do not own, that path is closed.
The cleaner approach is to fetch the original transcript as structured JSON, which gives you timed segments you control, and then run your own translation over them.
According to a Kapwing survey of content creators, only 43% of people translate their video content at all, yet 80% of viewers are more likely to finish a video when subtitles are available. That gap represents every video your users cannot fully engage with.
Step 1: Fetch the Transcript as Timed JSON
The YouTube Transcriber API returns transcripts as a JSON array of segments, each with a text string, a start time in seconds, and a duration in seconds. That structure maps directly onto subtitle cue format, which makes downstream conversion straightforward.
A basic request looks like this:
curl "https://getyoutubetranscriber.com/api/v2/transcript?video_url=dQw4w9WgXcQ&send_metadata=true" \
-H "Authorization: Bearer ytt_your_key_here"
The response:
{
"video_id": "dQw4w9WgXcQ",
"language": "en",
"transcript": [
{ "text": "We're no strangers to love", "start": 0.0, "duration": 2.5 },
{ "text": "You know the rules and so do I", "start": 2.5, "duration": 3.1 }
],
"metadata": {
"title": "Rick Astley - Never Gonna Give You Up",
"author_name": "RickAstleyVEVO"
}
}
The language field tells you the source language code, which is the first thing you need for translation. If you already know the target language track exists on YouTube, you can skip translation entirely by passing the lang parameter:
curl "https://getyoutubetranscriber.com/api/v2/transcript?video_url=dQw4w9WgXcQ&lang=fr" \
-H "Authorization: Bearer ytt_your_key_here"
If that track exists, you get it back directly. If not, the API falls back to the default track and you handle translation yourself. Always check response.language against your target to confirm whether translation is still needed.

Step 2: Translate Segments with DeepL or GPT
Once you have the segment array, you have two reasonable choices for translation.
DeepL is the better default for subtitle work. It handles European languages with high accuracy, supports auto source-language detection, and has a free developer tier that covers a meaningful volume of video content before you pay anything. Check the current limits on the DeepL pricing page, as they update periodically.
OpenAI GPT-4o works better for less common language pairs, heavily technical content, or situations where you need fine-grained control over formality and terminology through the system prompt.
Meta's NLLB (No Language Left Behind) is worth considering if you need an open-source self-hosted option covering 200 languages without per-character costs.
Python Example: Fetch, Translate, Export to SRT
import requests
import deepl
YOUTUBE_TRANSCRIBER_KEY = "ytt_your_key_here"
DEEPL_KEY = "your_deepl_key:fx"
TARGET_LANG = "DE" # DeepL language code for German
def get_transcript(video_id: str) -> dict:
resp = requests.get(
"https://getyoutubetranscriber.com/api/v2/transcript",
params={"video_url": video_id, "send_metadata": "true"},
headers={"Authorization": f"Bearer {YOUTUBE_TRANSCRIBER_KEY}"},
)
resp.raise_for_status()
return resp.json()
def translate_segments(segments: list, translator: deepl.Translator) -> list:
# Batch texts to reduce API round-trips
texts = [s["text"] for s in segments]
results = translator.translate_text(texts, target_lang=TARGET_LANG)
return [
{**seg, "text": res.text}
for seg, res in zip(segments, results)
]
def seconds_to_srt_time(seconds: float) -> str:
h = int(seconds // 3600)
m = int((seconds % 3600) // 60)
s = int(seconds % 60)
ms = int((seconds % 1) * 1000)
return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
def segments_to_srt(segments: list) -> str:
lines = []
for i, seg in enumerate(segments, start=1):
start = seconds_to_srt_time(seg["start"])
end = seconds_to_srt_time(seg["start"] + seg["duration"])
lines.append(f"{i}\n{start} --> {end}\n{seg['text']}\n")
return "\n".join(lines)
if __name__ == "__main__":
data = get_transcript("dQw4w9WgXcQ")
translator = deepl.Translator(DEEPL_KEY)
translated = translate_segments(data["transcript"], translator)
srt_output = segments_to_srt(translated)
with open("output_de.srt", "w", encoding="utf-8") as f:
f.write(srt_output)
print(f"Wrote {len(translated)} cues to output_de.srt")
Node.js Example: Fetch, Translate, Export to VTT
import fetch from "node-fetch";
import * as deepl from "deepl-node";
const YOUTUBE_TRANSCRIBER_KEY = "ytt_your_key_here";
const DEEPL_KEY = "your_deepl_key:fx";
const TARGET_LANG = "DE";
async function getTranscript(videoId) {
const url = new URL("https://getyoutubetranscriber.com/api/v2/transcript");
url.searchParams.set("video_url", videoId);
url.searchParams.set("send_metadata", "true");
const res = await fetch(url.toString(), {
headers: { Authorization: `Bearer ${YOUTUBE_TRANSCRIBER_KEY}` },
});
if (!res.ok) throw new Error(`Transcript fetch failed: ${res.status}`);
return res.json();
}
function secondsToVttTime(seconds) {
const h = Math.floor(seconds / 3600).toString().padStart(2, "0");
const m = Math.floor((seconds % 3600) / 60).toString().padStart(2, "0");
const s = Math.floor(seconds % 60).toString().padStart(2, "0");
const ms = Math.round((seconds % 1) * 1000).toString().padStart(3, "0");
return `${h}:${m}:${s}.${ms}`;
}
async function main() {
const data = await getTranscript("dQw4w9WgXcQ");
const translator = new deepl.Translator(DEEPL_KEY);
const texts = data.transcript.map((s) => s.text);
const results = await translator.translateText(texts, null, TARGET_LANG);
let vtt = "WEBVTT\n\n";
data.transcript.forEach((seg, i) => {
const start = secondsToVttTime(seg.start);
const end = secondsToVttTime(seg.start + seg.duration);
vtt += `${start} --> ${end}\n${results[i].text}\n\n`;
});
await fs.promises.writeFile("output_de.vtt", vtt, "utf8");
console.log(`Wrote ${data.transcript.length} cues to output_de.vtt`);
}
main().catch(console.error);
Step 3: Handle the Gotchas
Segment-level vs. sentence-level translation
The transcript endpoint returns segments as YouTube originally timed them, which means sentence boundaries rarely align with segment boundaries. A single sentence may span two or three segments, and a segment may contain half a sentence followed by the start of another.
If you translate each segment independently, the translation model sees incomplete clauses, which produces awkward output and sometimes incorrect grammar. The fix is to merge adjacent short segments before translating, then split the translated text back by approximate character ratio.
A simpler approach: concatenate all segment texts with a placeholder separator (for example |||), translate the entire block as one string, then split on the translated separator. This keeps sentence context intact across segments. Most translation APIs handle text up to 128,000 characters per request, so you can process most videos in a single call.
SEPARATOR = " ||| "
def translate_as_block(segments, translator):
joined = SEPARATOR.join(s["text"] for s in segments)
result = translator.translate_text(joined, target_lang=TARGET_LANG)
parts = result.text.split(SEPARATOR)
# Fall back gracefully if separator count drifts
if len(parts) != len(segments):
parts = result.text.split("|||")
return [{**seg, "text": parts[i].strip()} for i, seg in enumerate(segments)]
Rate limits on both sides
The YouTube Transcriber API handles YouTube's anti-bot measures for you, but the translation API has its own limits. A typical 10-minute YouTube video contains roughly 1,200 to 1,500 words, which translates to around 7,500 to 9,000 characters of source text. Check the current free-tier character quota on the DeepL pricing page to calculate how many videos you can process before costs kick in.
For bulk workloads, spread translation requests across time using a token bucket rate limiter, and use batch translation calls rather than per-segment requests. Both DeepL and the OpenAI API accept arrays of strings in a single request.
Cache translated output
Fetching and translating the same video in the same language more than once wastes both credits. A simple cache keyed by {video_id}:{target_lang} avoids repeat work. For the transcript fetch side, this is covered in detail in Caching YouTube Transcripts to Cut API Costs.
The pattern at the application layer:
import hashlib, json, pathlib
CACHE_DIR = pathlib.Path(".transcript_cache")
CACHE_DIR.mkdir(exist_ok=True)
def cache_key(video_id: str, lang: str) -> str:
return hashlib.sha256(f"{video_id}:{lang}".encode()).hexdigest()[:16]
def load_cached(video_id: str, lang: str):
path = CACHE_DIR / f"{cache_key(video_id, lang)}.json"
return json.loads(path.read_text()) if path.exists() else None
def save_cached(video_id: str, lang: str, segments: list):
path = CACHE_DIR / f"{cache_key(video_id, lang)}.json"
path.write_text(json.dumps(segments))
Keep a TTL. If the source video gets corrected captions after you cached the original auto-generated track, you want to re-fetch eventually.

Exporting to SRT vs. VTT
Both formats express the same underlying data: a cue index, a time range, and display text. The differences are small but matter depending on your target platform.
| Format | Header | Timestamp separator | Line end | Best for |
|------|------|----------------|-------|--------|
| SRT | None | Comma for milliseconds | Blank line | Most video editors, Premiere, DaVinci |
| VTT | WEBVTT | Period for milliseconds | Blank line | HTML5 <track> element, YouTube upload |
| VTT | Supports NOTE comments and CSS cue styling | | | Streaming platforms, web players |
YouTube accepts VTT uploads for manual caption tracks. If you are building a tool that pushes translated subtitles back to creators' own channels, VTT is the format to produce. For offline video editing workflows used by localization teams, SRT is more universally accepted.
The generate-srt-vtt functions in the Python and Node.js examples above already handle both. Swap the time separator and header as needed.
For a deeper look at generating subtitle files from raw transcript data, see Generate SRT and VTT Subtitles from YouTube Videos.
Real-World Use Case: Localization Teams at Scale
A localization team processing a library of 500 tutorial videos for a software product needs this pipeline to run reliably, not just once. The typical workflow:
- Pull a list of video IDs from the channel using the channel uploads endpoint.
- For each video, check the cache for each target language. Fetch transcript only on a cache miss.
- Batch all untranslated segments and send to DeepL in blocks of up to 50 segments.
- Store translated segments to cache and export SRT or VTT files.
- Upload finished subtitle files to the appropriate delivery system (your CDN, a video platform, or back to the creator's YouTube channel).
For managing the channel video list step, List All Videos from a YouTube Channel via API covers the channel uploads endpoint in detail.
The key operational concern at scale is idempotency: if a run fails partway through, you should be able to restart it and skip already-translated videos without re-charging your translation API. The file-based cache above handles this at the script level. For production systems, a database table with columns for (video_id, lang, status, created_at) is more robust.
One thing to validate before building: check whether the video already has a human-translated track in your target language by requesting it with the lang parameter first. If the API returns that language directly, you skip translation entirely. For a library of mixed-language content, spot-checking available tracks before queuing translation jobs can save a meaningful share of API costs.
Where to Go from Here
The YouTube Transcriber API starts you with 100 free credits and no credit card required. The API docs at getyoutubetranscriber.com/docs cover the transcript, search, channel, and playlist endpoints. If you are building a pipeline that needs to handle bulk downloads without hitting YouTube's anti-bot defenses, the post on downloading transcripts in bulk without getting blocked is worth reading before you start.
The code in this post is a working starting point. In practice you will want to add retry logic around both the transcript fetch and the translation call, structured logging for production debugging, and unit tests for the SRT/VTT formatting functions. But the core loop is simple enough that most teams can ship a first version in a single sprint.