# YouTube Transcripts + Speech AI (`steadyfetch/youtube-transcript-scraper`) Actor

Never charged for a video we can't transcribe. YouTube videos, Shorts and live VODs to text — captions first, built-in speech-to-text when a video has none, so caption-less videos still return real text. JSON, plain text, SRT or VTT. No API key, no cookies.

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

## Pricing

from $1.20 / 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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## YouTube Transcripts + Speech AI

**Never charged for a video we can't transcribe.** 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.

Issues answered in about 3 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` and `statusReason`, so you can reconcile the invoice from the
dataset itself without opening the console. **Only rows with `charged: true` were billed.**

***

### What a row looks like

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 |
| `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 |
| `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
Required:   videoUrls   (array of 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)
            maxItems              (integer, default 100 — hard cap on videos)
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
```

```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: **omit** an option you do not want rather than sending
`null` — the platform rejects an explicit `null` before the run is even created.

***

### 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 $1.20/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.

### 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 video is too long for the speech-to-text route — YouTube does not release enough of its audio for a complete transcript, and we never sell a partial one 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_speech_cap` | a limit stopped it; the row names which | yes |
| `input_error` | the link was not a YouTube video link | fix and re-run |

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. The speech
route works on short videos only: past a few minutes YouTube stops releasing the audio to
anything but its own player, so a long caption-less video comes back
`audio_too_long_for_speech` and uncharged rather than half-transcribed. 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.

**Why does a run cost more than the transcripts?**
Apify bills platform usage (compute and proxy) for what a run actually consumes, separately
from these events. `Maximum cost per run` is the ceiling that covers both.

***

### 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: All Transcripts](https://apify.com/steadyfetch/youtube-channel-transcripts) |

**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** · [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) |
| Amazon | [Products](https://apify.com/steadyfetch/amazon-product-scraper) · [Search](https://apify.com/steadyfetch/amazon-search-scraper) · Bestsellers · Sellers |
| Any media file | [Speech to Text](https://apify.com/steadyfetch/media-transcriber) · any link or file |

Unlinked names are publishing shortly on the same account — search **steadyfetch** on Apify Store.

Free n8n templates for the suite: **[github.com/steadyfetch/n8n-templates](https://github.com/steadyfetch/n8n-templates)** — no community nodes needed.

### Feedback & support

Found an issue? Open it on the **Issues tab** — issues are answered in about 3 hours, and always within one business day.

# Actor input Schema

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

YouTube video links (https://www.youtube.com/watch?v=…, https://youtu.be/…, /shorts/…, /live/…) or bare 11-character video IDs. Leave the default in place to get one uncharged sample row that shows the exact output shape. Omit the field rather than sending null.

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

Two-letter language code (en, es, ja) or a regional code (pt-BR). If the video has no captions in that language the default track 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.

## `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. Videos that would go past it are skipped as uncharged rows naming this limit.

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

Hard cap on how many videos this run processes.

## Actor input object example

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

# 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.

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

Delivered count, speech-to-text minutes charged, uncharged misses, failures, and the honest status message.

## `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/oLVQyy3syfrnmX3un/openapi.json
