# YouTube Transcript API & YouTube Subtitles Scraper — Speech AI (`steadyfetch/youtube-transcript-scraper`) Actor

YouTube videos, Shorts and live VODs to text: captions first, speech-to-text when a video has none (on by default; $0.008 per started minute, only when it runs). JSON, plain text, SRT or VTT. From $2.25 per 1,000 transcripts, no API key, no cookies. Never charged for a video we can't transcribe.

- **URL**: https://apify.com/steadyfetch/youtube-transcript-scraper.md
- **Developed by:** [Steadyfetch Team](https://apify.com/steadyfetch) (community)
- **Categories:** Videos, AI, Agents
- **Stats:** 105 total users, 62 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.25 / 1,000 transcripts

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## YouTube Transcript API & Scraper — YouTube Subtitles, Speech AI

**Never charged for a video we can't transcribe — or for one you already have.** Captions
first, built-in speech-to-text when a video has none, so caption-less videos still return
real text. Videos, Shorts and live VODs, as JSON, plain text, SRT or VTT.

**Coming back with more videos?** A published video's transcript does not change, so a schedule pays when its list grows. In Apify Console open **Schedules**, add this actor (or a saved task) with the same input — e.g. `{ "videoUrls": ["https://www.youtube.com/watch?v=jNQXAC9IVRw"], "newItemsOnly": true }` — or chain a scraper run into it with **Connect actor** (`datasetId`), and pick a cadence that matches how often new links arrive. Every video your account already got in the last 90 days comes back with nothing charged (`repeat: true`); with **New items only** on it is left out, so the dataset holds just the new ones. To follow a channel's uploads — weekly for most channels, daily for a channel that uploads daily — schedule steadyfetch/youtube-channel-transcripts instead.

**See a real run before you spend anything:** [sample dataset](https://api.apify.com/v2/datasets/5OZT4UwXAqLx5s8xe/items?clean=true\&format=json) — six videos from one run, six charged transcripts, each with its full text and timed segments. Unedited.

**Using an AI agent?** Pin this actor in Apify's MCP server with one link: `https://mcp.apify.com?tools=steadyfetch/youtube-transcript-scraper`

- **Actor id:** `steadyfetch/youtube-transcript-scraper`
- **Input:** `{ "videoUrls": ["https://www.youtube.com/watch?v=jNQXAC9IVRw"] }` — the one field you have to set.
- **Cap the bill:** set `maxTotalChargeUsd` on the run (a run option, not Actor input), e.g. `0.50` — the run stops when it reaches it.

**Just want to see it work?** Click **Start** with nothing set and the run transcribes one short real video (`youtube.com/watch?v=jNQXAC9IVRw`, "Me at the zoo"), charged like any run at your plan's per-transcript price. Set only settings — a different **Output format**, a caption language, speech-to-text off, a limit — and leave **Video URLs or IDs** empty, and that same sample runs under your settings, charged like any run. Put your own links or IDs in **Video URLs or IDs** for your own run.

![YouTube Transcript Scraper input form in the Apify console: Video URLs or IDs prefilled with two videos, above the collapsed transcript-options and cost-control sections](https://api.apify.com/v2/key-value-stores/wciMDNpNgBh87mGUp/records/youtube-transcript-scraper--input-form.png)

Issues answered in a couple of hours.
Unofficial: steadyfetch is **not affiliated with, endorsed by, or sponsored by YouTube or Google**. "YouTube" is a trademark of Google LLC, used here only to say what this actor reads.

Every row carries `charged`, `chargedEvents` (what that row cost, by event — `{}` when nothing) and
`statusReason`, and the last row of a run is its receipt (`_summary: true`): videos delivered and what the
whole run charged, by event. So you can reconcile the invoice from the dataset itself without opening the
console. **Only rows with `charged: true` were billed.**
If your **Maximum cost per run** is reached while a video's transcript is already in hand,
that transcript still ships in full with `charged: false` and a `statusReason` naming the
cap — your cap is never exceeded, and work already done is never thrown away.

***

### What a row looks like

![Dataset table of a real run: one row — “Me at the zoo” (jNQXAC9IVRw), manual captions, English, 19 seconds, with the transcript text and the video URL](https://api.apify.com/v2/key-value-stores/wciMDNpNgBh87mGUp/records/youtube-transcript-scraper--dataset-table.png)

Real output, unedited apart from trimming the segment list:

```json
{
  "status": "ok",
  "charged": true,
  "statusReason": null,
  "videoId": "jNQXAC9IVRw",
  "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
  "inputUrl": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
  "title": "Me at the zoo",
  "channelName": "jawed",
  "channelId": "UC4QobU6STFB0P71PMvOGN5A",
  "durationSeconds": 19,
  "viewCount": 406463143,
  "isLiveContent": false,
  "language": "en",
  "source": "captions",
  "captionKind": "manual",
  "text": "All right, so here we are, in front of the elephants the cool thing about these guys is that they have really... really really long trunks and that's cool (baaaaaaaaaaahhh!!) and that's pretty much all there is to say",
  "segments": [
    { "start": 1.2, "end": 3.36, "text": "All right, so here we are, in front of the elephants" },
    { "start": 5.318, "end": 7.974, "text": "the cool thing about these guys is that they have really..." }
  ],
  "srt": null,
  "vtt": null,
  "chargeEvents": { "transcript": 1, "speechMinutes": 0 }
}
```

| field | notes |
|---|---|
| `text` · `segments[{start,end,text}]` | the transcript, and the same text with timestamps in seconds |
| `srt` · `vtt` | filled only when you ask for that `format`; otherwise `null` |
| `source` | `captions` or `speech_ai` — how this transcript was produced |
| `captionKind` | `manual` (uploaded by the channel) or `auto` (YouTube's own auto-captions); `null` on the speech route |
| `language` | ISO code, same vocabulary on both routes; left blank on the rare transcript whose language could not be named |
| `title` · `channelName` · `channelId` · `durationSeconds` · `viewCount` · `isLiveContent` | as YouTube reports them |
| `url` · `inputUrl` · `sourceIndex` | the canonical watch URL, the exact link you passed, and its position in your input |
| `charged` · `statusReason` · `chargeEvents` | the reconciliation trio |
| `chargedEvents` | the same charge by event id — `{"transcript": 1, "speech_minute": 3}` on a charged row, `{}` on one that was not; the receipt's `chargedEvents` is the sum of every row above it |
| `_summary` (last row only) | the run's receipt, never charged: `status: "run_summary"`, `requested`, `delivered`, `chargedEvents` for the whole run, `stoppedBy` and the run's status line in `statusReason`. A run with **New items only** on has no receipt row, so your integration gets one row per new video and nothing else |
| `repeat` · `firstSeenAt` · `firstSeenRunId` | `true` when this account already had this transcript: it was handed back from the run named here, and nothing was charged for it |
| `retryable` | on a row that did not deliver: `true` means YouTube refused us this time and the same input is worth running again, `false` means the answer will not change. It always agrees with the note in `statusReason`, so code can branch on the column instead of parsing the sentence |

**Sample dataset — six real transcripts from one run, no sign-in:**
[open the JSON](https://api.apify.com/v2/datasets/5OZT4UwXAqLx5s8xe/items?format=json\&skipHidden=true)

### Agent / API paste-block

```
Actor:      steadyfetch/youtube-transcript-scraper
Set:        videoUrls   (the one field to set — YouTube video links or 11-character video IDs)
Optional:   language              (string, e.g. "en", "es", "pt-BR" — preferred caption track)
            format                (json | text | srt | vtt, default json)
            enableSpeechFallback  (boolean, default true — off = captions only, no speech minutes)
            maxSpeechMinutes      (integer, default 60 — hard cap for the whole run, up to 100,000)
            maxItems              (integer, default 500 — hard cap on videos, up to 5,000 per run)
Over-ask:   never refused — ask past a cap and the run still starts, continues at the
            ceiling above, and one uncharged row says what you asked for and what bound it
Charges:    transcript      once per delivered transcript, captions or speech-to-text
            speech_minute   per started minute, only when speech-to-text actually ran
Build spec: https://apify.com/steadyfetch/youtube-transcript-scraper/api
Token:      https://console.apify.com/settings/integrations
MCP:        pinned in one link — see the top of this page
```

```bash
curl -X POST "https://api.apify.com/v2/acts/steadyfetch~youtube-transcript-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"videoUrls":["https://www.youtube.com/watch?v=jNQXAC9IVRw","https://youtu.be/dQw4w9WgXcQ"],"format":"srt"}'
```

Calling from an agent or MCP client: every optional field accepts an explicit `null` and reads it
as **"use the default"**, so a template that renders one body per run and leaves the unset fields
`null` still runs — including a body where every field is `null`. A `null` in `videoUrls` runs the
one-video sample, exactly like an untouched Start, so send your real list. If you send an output format we do not recognise, the run
transcribes nothing, charges nothing, and returns one row naming the formats we do support.

Sending the same list under a sibling actor's field name works too: `urls` and `channels` are read
as this actor's video list — and so is `startUrls`, the name Apify's own link-list field uses, as plain
links or as `{"url": …}` objects — and one uncharged note row tells you the field is called `videoUrls`
here. A channel link inside either field still gets the row that points you at
`steadyfetch/youtube-channel-transcripts`, which is the actor that walks a whole channel.
Send the settings themselves as the run input; a body wrapped as `{"input": {...}}` is still read,
and one uncharged note row says so.

**MCP**: one link pins this actor in Claude, Cursor, or any MCP client — the link is at the top of this page — or ask Apify's MCP server for "youtube transcript".

***

### Use it as an API endpoint (Standby)

You can also call this actor like an API: send a few videos and get their transcripts back in the same response, with no run to start and no dataset to read. The address is `https://steadyfetch--youtube-transcript-scraper.apify.actor`, and the actor's **Endpoints** tab lets you try it from the browser.

```bash
curl -H "Authorization: Bearer <YOUR_APIFY_TOKEN>" \
  "https://steadyfetch--youtube-transcript-scraper.apify.actor/?videoUrls=dQw4w9WgXcQ,jNQXAC9IVRw&format=text"
```

Or POST the same input a run takes:

```bash
curl -X POST -H "Authorization: Bearer <YOUR_APIFY_TOKEN>" -H "Content-Type: application/json" \
  -d '{"videoUrls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"], "format": "srt"}' \
  "https://steadyfetch--youtube-transcript-scraper.apify.actor/"
```

The answer is JSON: `rows` (one per video, with the same fields as a row in a run's dataset), `receipt` (the run's closing summary row), `delivered`, `charged`, and `notes` for anything you should know, in plain words. You are charged exactly as in a run — per video that returns real text, plus each started speech-to-text minute when a video has no captions — and a video that cannot be transcribed is never charged. If the charge for a request cannot be completed, its transcripts are not sent.

Good to know: one request takes up to 25 videos and 60 speech-to-text minutes (5 videos and 10 minutes on the Apify free plan), and a video without captions longer than about 50 minutes does not fit one request — run the actor for it. The endpoint does not hand back videos your account already has, and does not read a `datasetId` or take `newItemsOnly`: every request is answered and charged on its own. For a weekly list with repeats handed back uncharged, run the actor on a Schedule as usual.

### Links you can paste

Watch links, `youtu.be` links, `/shorts/`, `/live/`, `/embed/`, `m.youtube.com`,
`music.youtube.com`, `youtube-nocookie.com`, and bare 11-character video IDs. Casing in the
link does not matter — chat apps and link shorteners rewrite it constantly — but the video
ID itself is case-sensitive, because YouTube treats it that way.

A channel link gets an uncharged row pointing you at
[steadyfetch/youtube-channel-transcripts](https://apify.com/steadyfetch/youtube-channel-transcripts), which does whole channels in one run. A playlist link
gets an uncharged row too — paste the individual video links here instead.

### What you are charged for

**Pricing: from $2.25/1,000 transcripts.** Two events, and nothing else:

- **Transcript** — once per video that returns real text, from captions or from speech-to-text.
- **Speech-to-text minute** — per started minute, and **only** when a video had no usable
  captions so speech-to-text had to run. A captioned video never triggers it, and a
  caption-less video too long for the speech route (see `audio_too_long_for_speech` below)
  is reported uncharged instead of being charged for a partial transcript.

**What can fail, and what it costs you: nothing.** The two most common non-deliveries are
`no_speech` (the audio is music or silence, so there is no transcript to sell) and
`blocked_retry` (YouTube challenged or throttled the fetch, or was still processing the video).
Both come back as a row with `charged: false` naming the reason — as do `no_captions`,
`private_or_members_only`, `removed_or_unavailable`, `age_restricted`, `region_blocked` and
`live_no_transcript_yet`. You pay for delivered transcripts and nothing else.

Switch **"Use speech-to-text when a video has no captions"** off and you will never be
charged a speech-to-text minute; caption-less videos come back as uncharged rows instead.
`Max speech-to-text minutes` and `Max videos` are hard stops, not suggestions: the run
finishes successfully and each skipped row names the limit that stopped it.
Raising that limit lets a re-run try the video; for a video without captions a re-run may still come
back uncharged without a transcript, because YouTube does not release the whole audio of every such video.

#### You are never charged twice for the same video

Re-run the same list, poll a video every week, paste a link you already had months ago —
a transcript this account has already been charged for is **handed back from the run that
produced it, with nothing charged for it a second time**. Nothing is fetched and no
speech-to-text minute is spent: the check happens before any of that.

Those rows carry `repeat: true`, `charged: false`, `firstSeenAt` (when you first got it)
and `firstSeenRunId` (which run), and `statusReason` says the same thing in words. They do
not count against `Max videos` either — your cap buys new videos.

The memory is a key-value store in your own Apify account, so a run started with a scoped
API token in restricted-access mode needs key-value store **Read, Write and Create**
permission (or Actor runs set to **Full access**) — set under *Settings → API &
Integrations* (Write is what lets the run remember what it delivered; without it every run
pays again). Without it the run says so on its status line and in an uncharged first row,
and it cannot skip what you already have. A run started from the console, or with a
full-access token, is unaffected.

The memory lives in **your own account**, in a key-value store called
**`yt-transcripts-account`** on your Storage tab. Delete it to start over and be charged
again. Transcripts drop out of it after 90 days on their own. If it cannot be read on a
given run, the run still delivers — it charges as it always did and the status line says
the check was unavailable, so you know that run could have billed a repeat. When the reason
is the token scope above, the line names the permission to grant instead.

Want the same thing for a whole channel every week — new uploads transcribed, the rest
handed back uncharged? That is
[steadyfetch/youtube-channel-transcripts](https://apify.com/steadyfetch/youtube-channel-transcripts),
the same engine with a channel as its input.

### Run it on a schedule — what a second run of the same list is worth

**A published video's transcript does not change.** Captions that exist today read the same
next week, and speech-to-text on the same audio returns the same words — so re-running a link
you already have brings back nothing new. Two things do change, and both come back uncharged:
a live stream has no transcript until it ends (`live_no_transcript_yet`), and `blocked_retry`
means YouTube throttled the fetch or was still processing that minute. Those are the rows worth
running again.

**A second run of the same list is not charged — for 90 days.** The account memory above is on
by default and there is no switch that turns it off: every video your account already has comes
back from the run that transcribed it, `repeat: true` and `charged: false`, with nothing fetched
and no speech-to-text minute spent. One thing can stop it: a scheduled run started with a scoped
API token that has no key-value store permission cannot open the memory, and then repeats are
transcribed and charged again — give the token key-value store Read, Write and Create, or set
Actor runs to Full access. It is per **account**, not per run or per Task, so a
colleague's Task on the same account sees it too. The edge worth knowing is the far end of that
window: entries drop out of `yt-transcripts-account` after 90 days, so a schedule pointed at a
fixed list runs uncharged for three months and then transcribes and charges those same videos
again. If your list is settled, run it once rather than on a timer.

**The schedule that pays is one whose list grows.** Save your input as a **Task**, put the Task
on an Apify **Schedule**, and add an integration or a webhook on *run succeeded* — a Google
Sheet, Slack, n8n, Make, or your own endpoint. Have whatever finds your videos write a fresh
`videoUrls` list into the Task each run — start it through the API, n8n or Make with the new
links — and every run transcribes only what your account has not had before; the links that
repeat are handed back, not charged, so the list can carry your whole history without the bill
growing with it. Watching one channel's uploads is the sibling actor above, which lists the
channel for you.

**Turn on `newItemsOnly` and a scheduled run delivers only the new videos.** By default a
repeat still arrives as a row — uncharged, `repeat: true` — which is right for an archive and
wrong for a webhook: your Sheet gets the whole list again every week and whatever reads it has
to de-duplicate. With **New items only** (`newItemsOnly: true`) a video your account already
has is left out of the dataset entirely: nothing fetched, nothing charged, no row, and the run's
status line says how many were skipped — *12 already yours, skipped (uncharged)*. A run whose
whole list was already yours succeeds with that count and an empty dataset, which is exactly
what an integration wants to see. Cost is unchanged either way: a repeat is never charged. The
one case it refuses is a run that cannot read the account memory at all — a scoped token with no
key-value store permission — because it cannot keep the promise, and charging you for the whole
back catalogue you asked it to skip is the wrong way to find out. Nothing is fetched or charged
then, and the rows say which permission to grant.

**Already running another scraper? Chain it — no script.** Put a finished run's dataset ID in
**Dataset ID (chain a scraper run)** (`datasetId`) and every row is searched for YouTube links,
however deeply your scraper nested them and whatever it called the field. In the Apify console,
**Connect actor** fills that field in for you, so a search-or-feed scraper plus this one becomes
a single two-step Task you can put on a Schedule. Rows that hold no YouTube link are counted and
said, never charged; a link already in **Video URLs or IDs** is one video and one charge; and a
bare 11-character string in someone else's row is never read as a video ID, so a dataset full of
post IDs and cursors cannot cost you anything. `datasetItems` takes the rows pasted directly if
you would rather not chain by ID. Both doors run through the same rules as a pasted link, so a
channel link inside a chained row still comes back with the pointer to the channel actor.

**`repeat` is the field to read.** `repeat: false` with `charged: true` is a transcript new to
your account; `repeat: true` means it was handed back from an earlier run, and `firstSeenRunId`
names that run. The status line carries the count for the whole run — *3 you already had —
handed back, not charged*.

### When a video is not charged

| `status` | what happened | temporary? |
|---|---|---|
| `no_speech` | the audio is music or silence — there is no transcript to sell | no |
| `no_audio_stream` | the video exposes no audio track at all | no |
| `no_captions` | no captions, and you switched speech-to-text off for this run | no |
| `asr_unavailable` | our speech-to-text service refused this actor's access mid-run — that is on us, not you. Captioned videos still delivered; caption-less ones came back uncharged | **yes — try again later** |
| `audio_too_long_for_speech` | no captions, and the whole audio could not be had — YouTube would not release it whole (or, rarely, it could not be cut into pieces) — and we never sell a partial transcript as whole. Captioned videos are unaffected at any length | no |
| `private_or_members_only` | private or members-only | no |
| `removed_or_unavailable` | YouTube says the video is gone; its own words are in the row | no |
| `age_restricted` | YouTube requires a signed-in, age-verified account | no |
| `region_blocked` | the uploader has not published it in the country we fetched from | no |
| `live_no_transcript_yet` | a live stream — no transcript exists until it ends | re-run after it ends |
| `blocked_retry` | YouTube challenged, throttled or was still processing | **yes — re-run** |
| `skipped_too_large` · `skipped_budget` · `skipped_deadline` · `skipped_speech_cap` | a limit stopped it before its transcript, and the row names which one: this actor's own audio size limit, your maximum cost per run, the run timeout (a short one names the seconds that fit), or the speech-minute cap (a transcript already in hand when the cost cap is reached is delivered instead, uncharged) | yes |
| `input_error` | the link was not a YouTube video link | fix and re-run |
| `sample_note` | you set settings but no videos, so the sample ran under them — the row names what you set, and is not charged | — |

A temporary problem is never reported as a permanent one. Bare video IDs are the one thing
we cannot sanity-check: a typo in an 11-character ID is indistinguishable from a real ID
that has been deleted, so it comes back as `removed_or_unavailable`, uncharged.

This actor may fail when the platform changes things — failed items are never charged.

### FAQ

**How do I get a YouTube transcript without an API key?**
Paste the video links and run it. There is no YouTube API key, no cookies and no sign-in.

**Can I download YouTube subtitles as SRT?**
Set `format` to `srt` (or `vtt`) and each row carries a ready-to-save subtitle string.

**What if a video has no captions?**
Speech-to-text runs on the audio and you get real text, marked `source: "speech_ai"`. If
the audio has no speech at all, the row comes back `no_speech` and uncharged. Long caption-less videos are transcribed too, up to 12 hours of audio; a longer one comes back `audio_too_long_for_speech`, uncharged, naming that limit.
Audio larger than one speech-to-text request is transcribed in pieces and joined, and its minutes
are charged once, on the whole video's length. A larger file comes back `skipped_too_large`,
uncharged, naming this actor's own limit; in the rare case YouTube will not release a video's
audio whole, the row comes back `audio_too_long_for_speech` and uncharged rather than half-transcribed. A caption-less video may also be skipped, uncharged, when transcribing it would cost more than it pays at the current speech price — its row comes back `speech_skipped_cost`. Videos that have captions — the large majority, including
nearly every spoken upload — are unaffected at any length.

**Can I pick the caption language?**
Set `language`. If that language is not published for the video, the default track is used
and the row's `language` field tells you what you actually got.

**Can I transcribe a whole channel?**
Use [steadyfetch/youtube-channel-transcripts](https://apify.com/steadyfetch/youtube-channel-transcripts) — paste a channel URL, `@handle` or channel ID
and it returns every video's transcript in one run. Playlists are not supported by either actor
yet; for a playlist, paste its video links here.

**Can I use this through an MCP server?**
Yes. It is a standard Apify actor, so any MCP client that can call Apify actors can call it.

**Does a run cost anything beyond these events?**
No. The price is all-inclusive: the platform usage a run consumes (compute and proxy) is paid by us and never
billed to you, and `Maximum cost per run` caps exactly the events above.

***

### Steadyfetch YouTube suite

Same transcript engine, different way in. All-inclusive pay per event, no start fee, charged only on delivery.

| What you paste | Actor |
|---|---|
| Video URLs or IDs | **this actor** |
| A channel URL, `@handle` or channel ID | [YouTube Channel Transcripts Scraper — Bulk Videos to Text](https://apify.com/steadyfetch/youtube-channel-transcripts) |
| A search phrase | [YouTube Search Scraper — Videos by Keyword + Transcripts](https://apify.com/steadyfetch/youtube-search-scraper) |

**The rest of the steadyfetch shelf** — same contract everywhere: all-inclusive pay per event,
no start fee, charged only on delivery.

| Family | Actors |
|---|---|
| Ad creative intelligence | [Facebook](https://apify.com/steadyfetch/facebook-ads-transcript-scraper) · [Google Ads video](https://apify.com/steadyfetch/google-ads-video-transcript-scraper) · [TikTok](https://apify.com/steadyfetch/tiktok-ads-transcript-scraper) · [LinkedIn](https://apify.com/steadyfetch/linkedin-ads-transcript-scraper) · [Google Ads text & OCR](https://apify.com/steadyfetch/google-ads-creative-text-scraper) |
| Trends & keywords | [Google Trends](https://apify.com/steadyfetch/google-trends-scraper) · [Trends Now](https://apify.com/steadyfetch/google-trends-now-scraper) · [Breakout keywords](https://apify.com/steadyfetch/breakout-keywords-scraper) · [Autocomplete keywords](https://apify.com/steadyfetch/google-keyword-suggest-scraper) · [Keyword volume & CPC](https://apify.com/steadyfetch/keyword-search-volume-scraper) · [Social trends](https://apify.com/steadyfetch/social-trends-scraper) |
| **YouTube transcripts** | [YouTube videos](https://apify.com/steadyfetch/youtube-transcript-scraper) · [YouTube channels](https://apify.com/steadyfetch/youtube-channel-transcripts) |
| Instagram | [Reel transcripts](https://apify.com/steadyfetch/instagram-reel-transcript-scraper) · [Profile posts](https://apify.com/steadyfetch/instagram-profile-posts) |
| Jobs | [Indeed](https://apify.com/steadyfetch/indeed-jobs-scraper) · [Career sites by domain](https://apify.com/steadyfetch/company-jobs-by-domain) · [Glassdoor](https://apify.com/steadyfetch/glassdoor-jobs-scraper) · [Multi-board](https://apify.com/steadyfetch/multi-job-board-scraper) · [Google Jobs](https://apify.com/steadyfetch/google-jobs-scraper) |
| Amazon | [Products](https://apify.com/steadyfetch/amazon-product-scraper) · [Search](https://apify.com/steadyfetch/amazon-search-scraper) · [Bestsellers](https://apify.com/steadyfetch/amazon-bestsellers-scraper) · [Sellers](https://apify.com/steadyfetch/amazon-seller-scraper) |
| Any media file | [Speech to Text · any link or file](https://apify.com/steadyfetch/media-transcriber) |

### Feedback & support

Found an issue? Open it on the **Issues tab** — we usually reply within a couple of hours, always within a day.

If it earned its keep, a rating helps other buyers find it, and saving the actor keeps it one click away.

# Changelog

This Actor's version history is a separate document: https://apify.com/steadyfetch/youtube-transcript-scraper/changelog.md

# Actor input Schema

## `videoUrls` (type: `array`):

YouTube video links or bare 11-character video IDs, one or hundreds a run (default 500), like https://www.youtube.com/watch?v=jNQXAC9IVRw; youtu.be, /shorts/, /live/, /embed/ links too. Never charged for a video we cannot transcribe or one this account already has; newItemsOnly true skips repeats. Null means “use the default”. Left untouched, click Start for a one-video sample at your plan’s per-transcript price; with only settings changed it runs under them, plus one uncharged row naming them.

## `datasetId` (type: `string`):

Paste the default dataset ID of a finished scraper run and every YouTube link in its rows is transcribed — no script, no copy-paste. With Apify’s "Connect actor" integration this is filled in for you, which is the cleanest way to schedule a chain. A link already in "Video URLs or IDs" is never transcribed twice, and a bare 11-character string in someone else’s row is never read as a video ID.

## `datasetItems` (type: `array`):

Paste rows from a scraper run instead of chaining by ID, and every YouTube link in them is transcribed. Works with any scraper’s output shape, including nested posts. Anything that is not a list of rows is ignored with one uncharged row saying so.

## `language` (type: `string`):

The caption language to prefer, as a two-letter code (en, es, ja) or a regional code (pt-BR). If the video has no captions in that language, the video's own default caption track (its original language) is used instead, and the row tells you which language you got.

## `format` (type: `string`):

json keeps timestamped segments, text returns the transcript only, srt and vtt add a ready-to-download subtitle string on the row. Leave it out, or send null, to get json. Anything else is not recognised: nothing is transcribed, nothing is charged, and one row names the formats that exist.

## `enableSpeechFallback` (type: `boolean`):

On: caption-less videos are transcribed with speech-to-text, which charges speech-to-text minutes on top of the transcript. Off: caption-less videos come back as uncharged rows and you never pay a speech-to-text minute.

## `maxSpeechMinutes` (type: `integer`):

Hard cap on speech-to-text minutes for the whole run (default 60), so a caption-less video longer than what is left of it comes back uncharged until you raise it. Videos that would go past it are skipped as uncharged rows naming this limit. 0 means no speech-to-text at all. Ask for more than this actor allows in one run and the run still starts: it continues at the actor's own ceiling of 100,000 minutes, with one uncharged row saying what was asked and what bound it.

## `maxItems` (type: `integer`):

Hard cap on how many videos this run processes (default 500). Ask for more than this actor serves in one run and the run still starts: it continues at the actor's own ceiling of 5,000 videos, with one uncharged row saying what was asked and what bound it.

## `newItemsOnly` (type: `boolean`):

ON for a scheduled run: delivers only videos this account has never had; the rest are left out of the dataset, so your Sheet, webhook or database gets one row per new video and nothing to de-duplicate. OFF (default): every video runs; one your account already has comes back from the earlier run, not charged (`repeat: true`). Either way a repeat costs nothing and the status line says how many were skipped. Only a delivered transcript counts, so a missed video is tried again next run.

## Actor input object example

```json
{
  "videoUrls": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "https://youtu.be/jNQXAC9IVRw"
  ],
  "format": "json",
  "enableSpeechFallback": true,
  "maxSpeechMinutes": 60,
  "maxItems": 500,
  "newItemsOnly": false
}
```

# Actor output Schema

## `transcripts` (type: `string`):

One row per video: transcript text, timestamped segments, optional SRT/VTT, channel, duration, language, and a status (ok / no_speech / removed_or_unavailable / age_restricted / live_no_transcript_yet / blocked_retry / …). Only rows with charged: true were billed, and each row's chargedEvents names what it cost; the last row (\_summary: true) is the run's receipt — never charged — with the whole run's chargedEvents (none on a New items only run). A transcript already in hand when your maximum cost is reached still delivers in full, uncharged. A row with repeat: true is a video this account already had: it is handed back from the run named in firstSeenRunId, with charged: false and no speech-to-text minutes.

## `summary` (type: `string`):

Delivered count, speech-to-text minutes charged, uncharged misses, failures, and the honest status message. On a run whose every delivered transcript was charged, note carries one line asking for a rating.

## `errors` (type: `string`):

Present only when a fetch or transcription failed after all retries: video ID and reason.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "videoUrls": [
        "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
        "https://youtu.be/jNQXAC9IVRw"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadyfetch/youtube-transcript-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "videoUrls": [
        "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
        "https://youtu.be/jNQXAC9IVRw",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("steadyfetch/youtube-transcript-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "videoUrls": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "https://youtu.be/jNQXAC9IVRw"
  ]
}' |
apify call steadyfetch/youtube-transcript-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steadyfetch/youtube-transcript-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/e7ArXViqT5t6hW1AG/builds/xBTMmGz445wwaI631/openapi.json
