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

YouTube Transcript API — Captions to Text, SRT & VTT

Pricing

from $0.57 / 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. Proxy included, no API key, no login.

Pricing

from $0.57 / 1,000 transcript returneds

Rating

0.0

(0)

Developer

Insight Solutions

Insight Solutions

Maintained by Community

Actor stats

0

Bookmarked

6

Total users

3

Monthly active users

9 days 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. $0.95 per 1,000 transcripts, proxy included ($5.00 from October 20, 2026), videos without captions are free, and a run that returns no transcripts costs nothing at all.

Try it in 30 seconds

Input — this is the Store prefill; paste it and run:

{
"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.

Output — one row per video; the fields you will use most are text, segments, language, title and durationSec (full list under Output reference). Anything that could not be fetched comes back as a free diagnostic row (ok: false, errorType, error) instead of a charge.

Price — $0.95 per 1,000 transcripts (from October 20, 2026: $5.00 per 1,000) on the FREE tier (+ $0.001 per run, charged only after the first transcript); volume tiers are Scale $0.76 and Business $0.57 per 1,000, and a video with no captions, a private or deleted video, an entry that was not a video, a video the run never reached and a fetch YouTube blocked are all free. Pay-per-event, no API key, no browser, limited permissions — works over the Apify MCP server (mcp.apify.com) and with agentic (x402) payments.

From code — client.actor("insight.solutions/youtube-transcript-api").call(run_input={…}) with apify-client, or POST https://api.apify.com/v2/acts/insight.solutions~youtube-transcript-api/run-sync-get-dataset-items.

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",
"proxyPath": "datacenter",
"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 subtitles — srt 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 datacenter proxy by default, one pinned session per parallel worker. When YouTube refuses an exit IP — HTTP 429, HTTP 403, a bot-check or consent interstitial, a player response saying "sign in to confirm you're not a bot", an empty caption file where the player response listed caption tracks, or a proxy-side 59x — that session is retired and the rung is retried once from a fresh address. Retrying on an address that was just refused only deepens the block, so it is never done.

If a fresh datacenter address is refused too, that one request is repeated through a residential exit — the player call or the caption download, whichever was refused, at most once per video. Residential transfer costs 40× what datacenter transfer does, so it is spent on the requests that need it and on nothing else. The row that comes back is an ordinary paid row and says proxyPath: "residential", so you can see exactly how often the expensive path was needed. Turn the behaviour off with residentialFallback: false and a refused video becomes a free blocked diagnostic row instead.

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.
  • Proxy is in the price, and it is not wasted. Requests go out through Apify datacenter proxy, and a request YouTube actually refuses is repeated once through a residential exit. Both are configured by default and the cost is inside the $0.95/1,000 — you are not billed for proxy traffic on top, and you are not paying for residential bytes on requests that never needed them.
  • 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 whose videos were all usable but produced nothing — no captions, a private video, a fetch YouTube blocked — finishes SUCCEEDED with zero results, a status message that says so, and a free diagnostic row per video telling you why; nothing is charged, start fee included. A run finishes FAILED only when there was nothing usable to attempt, or the Actor itself hit an error.
  • 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 datacenterThe exit every request starts on. Set {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]} to send everything residential instead — it works and costs 40× more per gigabyte. Clearing it sends requests from the run's own address
residentialFallbackbooleantrueRepeat a request YouTube refused through a residential exit, once per video. Off means a refused video gets a free blocked row instead

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
proxyPathWhich kind of exit carried it — datacenter, or residential when the datacenter exit was refused. Null on diagnostic rows
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

$0.95 per 1,000 transcripts. Pay-per-event, with the proxy already inside that number — there is no separate proxy line on your bill for this Actor, whether a transcript came through the datacenter exit or needed a residential retry.

This used to be $4.00 per 1,000. The Actor now sends requests through datacenter proxy and pays residential rates only for the individual request YouTube refuses — about one transcript in five — instead of routing everything residential, and the price came down with the cost, to $0.95.

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

Price change notice. From October 20, 2026 (UTC) the transcript event is $0.005 / $0.005 / $0.004 / $0.003 (FREE / Starter / Scale / Business); the table above applies until then.

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

  • 240 transcripts × $0.00095 = $0.228
  • 1 run start = $0.001
  • 10 videos without captions = $0.00
  • Total: $0.229

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 SUCCEEDED with zero results and a status message that says why ("0 transcripts. N diagnostic row(s) explain why …"), 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;
  • when a caption file does come back empty, it treats that as a refusal rather than an absence and repeats that one download through a residential exit, because the experiment's buckets are not the same on every address;
  • if every rung is gated and residential is refused too, 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 datacenter addresses with per-worker sessions and one rotation per block, and a request that is still refused after that is repeated once through a residential exit. 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 datacenter proxy is configured by default, a request YouTube refuses is repeated through a residential exit automatically, and the cost of both 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 still finishes SUCCEEDED, with zero transcripts, a status message that says so and nothing billed at all. Only an input with no usable video URL or ID in it finishes FAILED.

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