YouTube Transcript API — Captions to Text, SRT & VTT avatar

YouTube Transcript API — Captions to Text, SRT & VTT

Pricing

from $2.40 / 1,000 transcript returneds

Go to Apify Store
YouTube Transcript API — Captions to Text, SRT & VTT

YouTube Transcript API — Captions to Text, SRT & VTT

Get the transcript of any YouTube video as timed segments, plain text, SRT or VTT. Picks your preferred language, falls back to auto-generated captions and can machine-translate. Returns title, channel, duration and every available language. Residential proxy included, no API key, no login.

Pricing

from $2.40 / 1,000 transcript returneds

Rating

0.0

(0)

Developer

Insight Solutions

Insight Solutions

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

12 hours ago

Last modified

Share

Get the transcript of any YouTube video, as data. Give this Actor a list of YouTube links — or bare video IDs — and get back the spoken words as timed segments, plain text, SubRip (.srt) or WebVTT (.vtt), alongside the video's title, channel, duration, publish date and every caption language it offers.

No API key. No login. No cookies to paste. $4 per 1,000 transcripts, residential proxy included, videos without captions are free, and a run that returns no transcripts costs nothing at all.

Try it in 30 seconds

{
"videoUrls": [
"https://www.youtube.com/watch?v=iG9CE55wbtY",
"EJXTZ5jpSmk"
],
"languages": ["en"],
"format": "segments"
}

One watch URL, one bare video ID — both work, and so do youtu.be share links, Shorts, embeds and live URLs.

What comes back

{
"ok": true,
"videoId": "iG9CE55wbtY",
"url": "https://www.youtube.com/watch?v=iG9CE55wbtY",
"title": "Do schools kill creativity? | Sir Ken Robinson | TED",
"channelName": "TED",
"channelId": "UCAuUUnT6oDeKwE6v1NGQxug",
"durationSec": 1164,
"publishedAt": "2007-01-06T00:00:00.000Z",
"language": "en", // what you actually got
"languageName": "English",
"isAutoGenerated": false, // human-written caption file, not speech recognition
"isTranslated": false, // not machine-translated from another language
"languageFallback": false, // true only when none of your languages could be delivered
"availableLanguages": [ // every language this video offers, translations included
{ "languageCode": "en", "languageName": "English", "isAutoGenerated": false, "isTranslatable": true, "isTranslation": false },
{ "languageCode": "es", "languageName": "Spanish", "isAutoGenerated": false, "isTranslatable": true, "isTranslation": false }
],
"segments": [ // timings in seconds
{ "start": 12.32, "duration": 4.08, "text": "Good morning. How are you?" },
{ "start": 16.4, "duration": 3.52, "text": "It's been great, hasn't it?" }
],
"text": "Good morning. How are you? It's been great, hasn't it? …",
"wordCount": 2543,
"charCount": 14208,
"retrievedVia": "innertube-android",
"scrapedAt": "2026-09-09T11:04:22.311Z",
"source": "youtube.com",
"sourceUrl": "https://www.youtube.com/api/timedtext?v=iG9CE55wbtY&lang=en&fmt=json3"
}

Ask for "format": "srt" or "format": "vtt" and the row gains an srt or vtt field holding a complete, valid subtitle file — numbered cues and HH:MM:SS,mmm for SubRip, a WEBVTT header and HH:MM:SS.mmm for WebVTT, with overlapping cues clamped so a player never doubles a line. Ask for "format": "text" and you get the prose without the segment array.

Use cases

  • Summarise or search long-form video — pull text straight into an LLM prompt, with wordCount there so you can size the prompt before you send it.
  • Build a searchable video archive — one row per video, timings intact, so a search hit can link to youtube.com/watch?v=ID&t=42s.
  • Repurpose a channel's back catalogue into articles, show notes or newsletters.
  • Ship subtitlessrt and vtt come out ready to attach to a re-upload or a player.
  • Feed a research or RAG pipeline — segments carry timestamps, so a citation can point at the second the claim was made.
  • Translate a talk — ask for a language the video does not have and YouTube's own machine translation fills it in, flagged as isTranslated.

How it works, and why it keeps working

YouTube has no public captions API. Everything that reads transcripts reads the same private endpoint the YouTube apps use — and over 2025 and 2026 that endpoint has been progressively fenced off. Single-path scrapers break every few months. This Actor runs a chain of five paths and takes the first that answers:

RungWhat it isWhy it is on the list
1InnerTube player, Android clientLoosest validation of the four clients, and the fastest path
2InnerTube player, Android VR clientThe client that is not subject to the proof-of-origin token requirement — the answer when rung 1 returns a token-gated caption URL
3InnerTube player, iOS clientA third policy bucket; occasionally the only one serving a given video
4InnerTube player, Web clientThe browser context. Most likely to be token-gated, so it goes last
5Watch page ytInitialPlayerResponseThe same document scraped from HTML. Slow, but it is an ordinary page load and survives some API-level refusals

Under all five: Apify residential proxy, one pinned session per parallel worker. When YouTube refuses an exit IP — HTTP 429, HTTP 403, or a player response saying "sign in to confirm you're not a bot" — that session is retired and the rung is retried once from a different residential address. Retrying on an address that was just refused only deepens the block, so it is never done.

The retrievedVia field on every row says which rung produced it. If you are monitoring this Actor, that column is the early warning: a shift from innertube-android to the later rungs means the front of the chain is degrading, well before anything actually fails.

How it compares

  • Five fallback paths, not one. Most transcript scrapers implement a single client context and break the week YouTube changes it.
  • Residential proxy is in the price. YouTube blocks datacenter IPs wholesale, so a transcript scraper without residential egress does not work from a cloud host at all. The proxy is configured by default and the cost is inside the $4/1,000 — you are not billed for proxy traffic on top.
  • Failures are free and legible. A video with no captions, a private video or a blocked fetch produces a diagnostic row with an errorType you can branch on — and no charge. A run that returns nothing at all finishes FAILED with the reason in its status message, never a green run containing an apology.
  • Language handling that says what it did. You get language, isAutoGenerated, isTranslated and the full availableLanguages list, so a fallback is never silent.

Input reference

FieldTypeDefaultWhat it does
videoUrls (required)array of stringsprefilled with two captioned videosWatch URLs, youtu.be links, Shorts, embed or live URLs, or bare 11-character video IDs. Duplicates are fetched, and billed, once
languagesarray of strings["en"]Language codes in order of preference. See the fallback order below
formatsegments | text | srt | vttsegmentsThe primary representation. srt and vtt are only added to the row when you ask for them
includeTimestampsbooleantrueKeep the segments array. Turn off for a much smaller row
includeMetadatabooleantrueFill in title, channel, duration and publish date. Costs no extra request
maxConcurrencyinteger4Videos in parallel. Each worker keeps its own proxy session
videoTimeoutSecsinteger30The whole budget for one video, across every rung, retry and the caption download. Any single request is additionally capped at 15 s
maxRunSecsinteger240Whole-run wall-clock budget. When it runs out the Actor keeps what it has and files a free diagnostic row for each video it never reached
proxyConfigurationobjectApify residentialLeave it alone. Clearing it sends requests from the run's datacenter address, which YouTube will block

How languages resolves, in order — and the row always says which step won:

  1. a human-written track in one of your languages (exact code first, then the base language: en accepts en-GB);
  2. an auto-generated track in one of your languages;
  3. a machine translation into one of your languages — the translation is always attempted when the track is translatable, even if YouTube's response did not list your language as a target;
  4. any human-written track the video has;
  5. any auto-generated track.

Output reference

Every row carries the same keys. ok: true is a transcript; ok: false is a free diagnostic row.

FieldWhat it is
videoId, url, inputThe video, its canonical watch URL, and the entry you supplied
title, channelName, channelId, durationSec, publishedAtVideo metadata, when includeMetadata is on
language, languageNameThe language you actually got
isAutoGeneratedtrue when the text came from speech recognition rather than a human-written caption file
isTranslatedtrue when YouTube machine-translated an existing track for you
languageFallbacktrue when none of your languages could be delivered, so language is something else
availableLanguages[]Every language the video offers, machine-translation targets included
segments[]{ start, duration, text } in seconds
textEvery segment joined with single spaces
srt / vttA complete subtitle file — present only when that format was requested
wordCount, charCountSize of text
ok, error, errorTypeWhether this row is a transcript, and if not, why not
retrievedViaWhich rung of the fallback chain produced it
scrapedAt, source, sourceUrlWhen, from where. The caption URL's expiring signature is stripped before it is written

errorType on a diagnostic row is one of:

ValueMeaningCharged?
no-captionsThe video has no caption track at allNo
privatePrivate, members-only or age-restricted, so a signed-in account would be needed. This Actor never signs inNo
unavailableDeleted, region-locked or otherwise unplayableNo
blockedYouTube refused our requests, including when the caption file is gated behind a proof-of-origin tokenNo
invalid-idThe entry was not a YouTube video — a channel, a playlist, or something elseNo
timeoutThe run's maxRunSecs budget ran out before this video was reachedNo

Pricing

$4.00 per 1,000 transcripts. Pay-per-event, with the residential proxy already inside that number — there is no separate proxy line on your bill for this Actor.

EventWhat triggers itFREEStarterScaleBusiness
Transcript returned (primary)One transcript written to your dataset$0.004$0.004$0.0032$0.0024
Run startedOnce per run, after the first transcript$0.001$0.001$0.001$0.001

Worked example. 250 videos, of which 240 have captions:

  • 240 transcripts × $0.004 = $0.96
  • 1 run start = $0.001
  • 10 videos without captions = $0.00
  • Total: $0.961

What you are never charged for: a video with no captions, a private or deleted video, an entry that was not a video, a video the run never reached before maxRunSecs, or a fetch YouTube blocked. If a whole run comes back empty it finishes FAILED and bills nothing at all, start fee included.

Set ACTOR_MAX_TOTAL_CHARGE_USD on a run and the Actor stops fetching once the ceiling is in sight, rather than handing you rows it cannot bill or billing you for rows it cannot hand over.

Limits, and the one that might bite

Videos without captions. Plenty of YouTube has no caption track at all — short clips, music videos, some live streams, and anything an uploader has explicitly opted out of. There is nothing to fetch; you get a free no-captions row saying so.

Age-restricted, private and members-only videos. These need a signed-in account. This Actor does not log in, does not accept cookies and does not take a session token, and it never will — that is a deliberate line, not a missing feature. They return a free private row.

Proof-of-origin ("PO") tokens — the real risk. Through 2025 and 2026 YouTube has been rolling out an experiment that marks a caption URL with exp=xpe, and the caption endpoint then answers HTTP 200 with an empty body unless the request carries a token minted by YouTube's own player JavaScript. There is no way to produce that token without running YouTube's JavaScript in a browser.

What this Actor does about it:

  • it detects the marker on the caption URL before spending a request on it;
  • it moves to the next client in the chain, because the clients are gated independently — an Android VR caption URL is routinely clean when the Android or Web one is not. This is the single reason rung 2 exists;
  • if every rung is gated, it writes a free blocked diagnostic row whose error says exactly that, rather than an empty transcript or a silent success.

Today the chain gets past this for the overwhelming majority of videos. If YouTube ever extends the token requirement to every client context, transcripts would stop being retrievable by any means short of a full browser — for this Actor and for every other tool of its kind. We would say so on this page rather than quietly returning worse data, and the run would still cost you nothing.

Rate and reliability. Requests go out through residential addresses with per-worker sessions and one rotation per block. Four videos in parallel is the default because it is where throughput and block rate balance; raising maxConcurrency speeds a long list up and makes blocks more likely.

Use it from an AI agent, or from code

One JSON object in, one flat array out — the shape agent runtimes want. The Actor runs with limited permissions, uses pay-per-event pricing and never enters Standby, so it works over the Apify MCP server and with x402 agentic payments. The Integrations tab pushes results to Slack, a webhook, Zapier, Make, Google Sheets, Snowflake or BigQuery.

curl -X POST "https://api.apify.com/v2/acts/insight.solutions~youtube-transcript-api/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"videoUrls":["iG9CE55wbtY"],"format":"text"}'
# pip install apify-client
from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("insight.solutions/youtube-transcript-api").call(run_input={
"videoUrls": [
"https://www.youtube.com/watch?v=iG9CE55wbtY",
"https://youtu.be/EJXTZ5jpSmk",
],
"languages": ["en"],
"format": "segments",
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
if row.get("ok"):
print(row["title"], row["wordCount"], "words", sep=" | ")
else:
print("skipped:", row["input"], row["errorType"])

Set "format": "text" and "includeTimestamps": false when you only need the words — the payload shrinks by roughly half and costs the same.

FAQ

Does this work on any video? Any public video that has a caption track. Videos with captions turned off, and private, members-only or age-restricted videos, return a free diagnostic row instead.

Are auto-generated captions any good? They are usable and they are clearly labelled — isAutoGenerated: true. Expect almost no punctuation, sentence boundaries you cannot trust, and mangled proper nouns. A human-written track is always preferred when the video has one.

Can I get a language the video does not have? Often, yes. If any track is translatable, the Actor asks YouTube for your language and you get a machine translation, flagged isTranslated: true. If YouTube serves nothing for that language, you get the original track and the row says so (languageFallback: true). The quality is YouTube's machine translation, not ours.

Why do I sometimes get a different language than I asked for? Because the video did not offer yours and could not translate into it. The row still tells you exactly what you got — language, plus the full availableLanguages list, so you can decide what to do next.

Do I need my own proxy? No. Apify residential proxy is configured by default and its cost is inside the per-transcript price.

Will it handle a playlist or a whole channel? Not yet — this Actor takes videos, one row each. Feed it a list of video URLs from wherever your list comes from.

What happens if one video fails? The others still run. The failed one produces a free diagnostic row and the run finishes SUCCEEDED. If every video fails the run finishes FAILED and you are billed nothing at all.

How long can a run take? maxRunSecs, 240 seconds by default, checked before each video. When it runs out the Actor stops fetching, keeps and bills for everything already collected, files a free timeout row for each video it never reached, and finishes.

Is the data fresh? Live. Every run reads YouTube at that moment; nothing is cached.

  • Public videos only. Every source is a public video page or the public caption endpoint behind it. The Actor never logs in, never accepts cookies or session tokens, never takes an API key belonging to anyone else, and never touches private, members-only or age-restricted content.
  • Captions are the uploader's copyright. This Actor retrieves them; it does not license them. Republishing a transcript, or training on it, is your call and your responsibility, subject to YouTube's terms and to copyright law where you operate. Quoting, summarising, searching and indexing are the ordinary uses and are what this is built for.
  • Expiring credentials are stripped. The caption URL YouTube signs is a short-lived bearer credential. sourceUrl records where the transcript came from with that signature removed.
  • No personal data by design. A transcript is the spoken content of a public video. The Actor stores no viewer data, no account data and no cookies, and it writes only the video's own public metadata alongside the text.
  • Not affiliated with YouTube, Google LLC, or with any channel or creator whose video you retrieve. All product names and trademarks belong to their respective owners and are used only to describe which public endpoints this Actor reads.

Our other Actors

Every Insight Solutions Actor is pay-per-result with no browser, no login and no API key, and every one of them returns free diagnostic rows instead of billing for failures. Prices are per 1,000 results.

Video, audio & social

News, documents & the web

Business, finance & jobs

Apps & games