# YouTube Shorts Transcript Scraper - Bulk Shorts to Text (`sauliusautomatesit/youtube-shorts-transcript-scraper`) Actor

Transcribe YouTube Shorts to text, timestamped segments, SRT and VTT in bulk. Uses YouTube's own caption track when there is one and falls back to Whisper speech-to-text when there is not. No API key, no login, no cookies.

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

## Pricing

from $1.70 / 1,000 transcript from native captions

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 Shorts Transcript Scraper — bulk Shorts to text

**Turn YouTube Shorts into transcripts, timestamps, SRT and VTT — in bulk.**

Paste Shorts links, get the words back. Every Short returns plain text, timestamped segments,
a ready-to-use `.srt` subtitle file, a `.vtt` file, and the source metadata: channel, title,
hashtags, duration, language, view count.

YouTube captions most Shorts itself, and this Actor reads that track first — which is fast and
costs $0.002 a video. A Short with no usable caption track is transcribed from its audio with
Whisper instead, so a caption-less Short is not simply skipped.

- **No API key, no login, no cookies.**
- **90+ languages.** The transcript comes back in the video's own language by default.
- **Bulk-safe.** One dead link never breaks the rest of the run.
- **You only pay for delivered transcripts.** Failures, private videos and over-length videos
  are free.

***

### What it handles

| Input | Supported |
|---|---|
| `youtube.com/shorts/VIDEOID` | ✅ |
| `youtu.be/VIDEOID` | ✅ |
| `youtube.com/watch?v=VIDEOID` | ✅ — the same video, whichever URL you copied |
| `youtube.com/embed/VIDEOID`, `m.youtube.com/…`, `youtube-nocookie.com/…` | ✅ |
| Regular (non-Short) YouTube videos | ✅ from captions, up to `maxDurationSeconds` |
| Channel, playlist or search URLs | ❌ — not single videos; returned as `INVALID_URL` |
| Private, deleted, age-restricted or members-only videos | ❌ — returned as an error row, uncharged |
| Live streams | ❌ — `LIVE_VIDEO`, uncharged |

Other platforms return a clear error pointing you at
[Short Video Transcriber](https://apify.com/sauliusautomatesit/short-video-transcriber), which
handles TikTok and YouTube Shorts in one run.

***

### Captions vs Whisper — and why it matters to your bill

1. **YouTube's caption track first.** Creator-written subtitles when they exist, YouTube's own
   auto-captions otherwise. Charged **$0.002**.
2. **Whisper when there is none.** The audio is downloaded and transcribed locally with
   faster-whisper. Charged **$0.040**, because it costs roughly a second of CPU per 1.5
   seconds of audio.

YouTube auto-captions the great majority of spoken-word Shorts, so most runs sit near the
$0.002 end. Exactly one of those two events is charged per video, after the row is written —
never both, never before. A video that fails is not charged at all.

***

### What you get

One dataset row per input URL:

```json
{
  "url": "https://www.youtube.com/shorts/BDf5mOfSkkw",
  "platform": "youtube_shorts",
  "videoId": "BDf5mOfSkkw",
  "author": "UCUvUKGDBCqJzV4OwJkQWCHg",
  "authorName": "King Arthur Baking Company",
  "title": "The Only Sourdough Recipe You'll Ever Need",
  "hashtags": ["baking", "sourdough"],
  "durationSeconds": 15.0,
  "language": "en",
  "source": "native_captions",
  "sourceDetail": "youtube_shorts_auto_captions",
  "isAutoGenerated": true,
  "text": "I'm going to show you how to make the only sourdough bread that you need to know…",
  "segments": [
    { "start": 0.0,  "end": 1.14, "text": "I'm going to show you how to make the" },
    { "start": 1.14, "end": 2.46, "text": "only sourdough bread that you need to" }
  ],
  "srt": "1\n00:00:00,000 --> 00:00:01,140\nI'm going to show you how to make the\n\n2\n…",
  "vtt": "WEBVTT\n\n00:00:00.000 --> 00:00:01.140\nI'm going to show you how to make the\n\n…",
  "wordCount": 62,
  "segmentCount": 9,
  "viewCount": 184231,
  "status": "ok",
  "error": null
}
```

`source` is always exactly one of:

- `native_captions` — the words came from YouTube's caption track.
- `whisper_asr` — the video had no usable captions, so the audio was transcribed with Whisper.

The `srt` and `vtt` fields are complete, valid subtitle files. The scrolling overlaps that
YouTube's auto-caption track produces are trimmed, so subtitles do not double up on screen.

***

### Pricing

| Event | Price | When it fires |
|---|---|---|
| `caption_transcript` | **$0.002** | A transcript came from YouTube's caption track. |
| `asr_transcript` | **$0.040** | A transcript came from Whisper because there were no captions. |
| `apify-actor-start` | **$0.00005** | Apify's standard start event, charged once per gigabyte of the run's memory. This Actor runs at 2 GB, so **$0.0001 per run**. |

Those are the Free-plan prices. Apify's paid plans get the standard Store discount off every
event — Bronze 5 %, Silver 10 %, Gold and above 15 % — so `asr_transcript` costs $0.038, $0.036
or $0.034 on those plans and `caption_transcript` costs $0.0019, $0.0018 or $0.0017.

#### What a run actually costs

| 1,000 Shorts | Cost |
|---|---|
| All captioned | **$2.00** |
| 90 % captions / 10 % Whisper | **$5.80** |
| 70 % captions / 30 % Whisper | **$13.40** |
| None captioned | **$40.00** |

(Free-plan prices; a Gold plan pays 15 % less than each of these.)

Turn **"Transcribe with Whisper when a Short has no captions"** off to cap the price at $0.002
per video: caption-less Shorts then return a `CAPTIONS_UNAVAILABLE` error, free of charge. On
YouTube that is a reasonable default for large batches, because the caption coverage is good.

**Nothing else is charged.** In particular you are not charged for:

- videos that are deleted, private, age-restricted or members-only;
- live streams;
- videos longer than your `maxDurationSeconds` cap;
- caption-less videos longer than `maxAsrDurationSeconds` (they return `ASR_DURATION_EXCEEDED`);
- videos where Whisper finds no speech at all (music-only clips);
- channel URLs, malformed URLs or links from other platforms;
- any run that fails.

***

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `videoUrls` | array | — | **Required.** YouTube Shorts URLs, one per line. `youtu.be` and `watch?v=` links work. |
| `language` | string | *(blank)* | Two-letter code (`en`, `es`, `pt`…). Blank keeps the video's own language. Setting it selects that language's caption track when the channel published one, and tells Whisper what to expect. |
| `allowWhisperFallback` | boolean | `true` | Off = captions only, and caption-less Shorts cost nothing. |
| `maxDurationSeconds` | integer | `600` | Videos longer than this are skipped, free. Max `1200`. |
| `maxAsrDurationSeconds` | integer | `180` | Length limit for the Whisper path only — exactly the length of the longest Short. Caption-less videos longer than this return `ASR_DURATION_EXCEEDED`, free. Captioned videos are unaffected. |
| `maxVideos` | integer | `1000` | Safety cap on the number of URLs processed. |
| `forceWhisper` | boolean | `false` | **Advanced.** Transcribe with Whisper even when captions exist. Charged at the `asr_transcript` rate — 20× the caption price. |
| `whisperModel` | string | `base` | `tiny`, `base` or `small`. Only affects the Whisper fallback. |
| `beamSize` | integer | `1` | Whisper decoding beam width. |
| `concurrency` | integer | `5` | Videos resolved in parallel. Whisper always runs one at a time. |
| `includeFailedItems` | boolean | `true` | Keep a row (with an `error` code) for every failure, so inputs reconcile to outputs. |
| `useResidentialFallback` | boolean | `true` | **Leave this on.** YouTube refuses datacenter IPs outright; this retries those requests through a residential IP. |
| `proxyConfiguration` | object | Apify Proxy | Recommended. Used for the first attempt on every request. |

#### Minimal input

```json
{
  "videoUrls": [
    "https://www.youtube.com/shorts/BDf5mOfSkkw",
    "https://youtu.be/pM62yIJNuao"
  ]
}
```

***

### Run it

#### In the Apify Console

Open the Actor, paste your links into **YouTube Shorts URLs**, one per line, and click
**Start**. Results appear in the dataset and export as JSON, CSV, XLSX or Excel — or download
the `srt` column straight into subtitle files.

#### API

```bash
curl -X POST "https://api.apify.com/v2/acts/sauliusautomatesit~youtube-shorts-transcript-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "videoUrls": [
          "https://www.youtube.com/shorts/BDf5mOfSkkw"
        ],
        "allowWhisperFallback": true
      }'
```

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_TOKEN")

run = client.actor("sauliusautomatesit/youtube-shorts-transcript-scraper").call(input={
    "videoUrls": [
        "https://www.youtube.com/shorts/BDf5mOfSkkw",
        "https://www.youtube.com/shorts/pM62yIJNuao",
    ],
})

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item["status"] != "ok":
        print(f"{item['inputUrl']}: {item['error']} — {item['errorMessage']}")
        continue
    print(item["authorName"], item["language"], item["wordCount"], "words", f"({item['source']})")
    with open(f"{item['videoId']}.srt", "w", encoding="utf-8") as handle:
        handle.write(item["srt"])
```

#### JavaScript

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_TOKEN' });

const run = await client.actor('sauliusautomatesit/youtube-shorts-transcript-scraper').call({
    videoUrls: ['https://www.youtube.com/shorts/BDf5mOfSkkw'],
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();

for (const item of items) {
    if (item.status !== 'ok') {
        console.warn(`${item.inputUrl}: ${item.error}`);
        continue;
    }
    console.log(`${item.authorName} (${item.source}): ${item.text}`);
    // await fs.writeFile(`${item.videoId}.srt`, item.srt);
}
```

#### n8n, Make and Zapier

Works with the standard **Apify → Run Actor** node in n8n and the **Apify → Run an Actor**
module in Make. Point the node at this Actor, pass `videoUrls` as the input JSON, then read the
dataset items in the next step — `text` for the transcript, `srt` for a subtitle file, `error`
to branch on failures.

A common shape: a sheet or webhook supplies Shorts links → this Actor returns transcripts → an
LLM node summarises, tags or repurposes them → the result is written back. Because failures
come back as rows with an `error` field rather than as a broken run, a filter on
`status == "ok"` is all the error handling most workflows need.

***

### Errors

Failed videos get a dataset row with `status: "error"`, an `error` code and a plain-English
`errorMessage`. They are never charged.

| Code | Meaning |
|---|---|
| `VIDEO_UNAVAILABLE` | Deleted, never existed, or region-locked. |
| `VIDEO_PRIVATE` | Private or members-only. |
| `LOGIN_REQUIRED` | YouTube demanded a signed-in session. Turn `useResidentialFallback` on. |
| `AGE_RESTRICTED` | Age-restricted or behind a content warning. |
| `LIVE_VIDEO` | A live stream, which cannot be transcribed. |
| `DURATION_EXCEEDED` | Longer than `maxDurationSeconds`. |
| `ASR_DURATION_EXCEEDED` | No captions, and longer than `maxAsrDurationSeconds`. Raise that limit to transcribe it anyway. |
| `CAPTIONS_UNAVAILABLE` | No captions and the Whisper fallback is turned off. |
| `NO_SPEECH_DETECTED` | Whisper found no speech — a music-only or silent video. |
| `INVALID_URL` | Not a link to a single video — a channel or playlist URL, for example. |
| `PLATFORM_NOT_ENABLED` | A TikTok or other non-YouTube URL. |
| `MEDIA_UNAVAILABLE` | The audio could not be downloaded for transcription. |
| `MEMORY_LIMIT` | The video does not fit in the run's memory for the chosen model. Raise the memory or pick a smaller model. |

***

### Limits and good to know

- **Leave the residential retry on.** YouTube answers datacenter addresses — Apify Proxy's and
  the platform's own alike — with "Sign in to confirm you're not a bot", for videos that are
  perfectly public. This is not optional cost-shaving: with `useResidentialFallback` off,
  nearly every Short returns `LOGIN_REQUIRED`. Only the requests the cheap route already
  refused go over residential, and caption downloads stay on the cheap route, so the extra
  traffic is a few hundredths of a cent per video.
- **Public videos only.** No login, no cookies, no members-only or age-restricted content.
- **Auto-captions are auto-captions.** When a transcript comes from YouTube's own ASR track
  (`isAutoGenerated: true`), it carries that track's mistakes — including missing punctuation
  on older tracks. For maximum accuracy on a specific video, switch `forceWhisper` on and
  accept the `asr_transcript` price, or raise `whisperModel` to `small`.
- **Caption availability changes.** YouTube backfills captions hours after upload, so the same
  video can take the Whisper path today and the caption path tomorrow.
- **Whisper is length-limited; captions are not.** A caption-less video is transcribed up to
  `maxAsrDurationSeconds` (180 s by default — exactly the maximum length of a Short). Past
  that, Whisper's cost outgrows a per-video price, so the video returns
  `ASR_DURATION_EXCEEDED` and costs you nothing. Videos **with** captions run to the full
  `maxDurationSeconds` at the $0.002 rate, whatever their length.
- **Memory.** Run with at least 2 GB. That covers the whole duration range with the `base`
  model; the Actor refuses an impossible combination up front instead of being killed mid-run.
- **One URL per video.** This Actor does not crawl channels or playlists to find Shorts for you.

### What this Actor does not do

It transcribes videos. It does not scrape comments, channel statistics or search results, does
not monitor channels, and does not summarise, translate or score sentiment — pipe the
transcript into an LLM step for that.

***

### Related searches

YouTube Shorts transcript · Shorts to text · transcribe YouTube Shorts · YouTube Shorts
subtitles · Shorts SRT · YouTube Shorts caption extractor · bulk Shorts transcripts ·
YouTube subtitle downloader · Shorts speech to text · YouTube Shorts VTT

# Actor input Schema

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

YouTube Shorts URLs to transcribe — one per line, from a single link to thousands. `youtube.com/shorts/ID`, `youtu.be/ID` and `youtube.com/watch?v=ID` all work, so a Short you copied from the watch page needs no editing. Channel and playlist URLs are not single videos and will be reported as errors.

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

Two-letter language code (`en`, `es`, `pt`, `de`, `hi`…). Leave blank to keep the video's own language — the default, and what you want for a transcript. Setting it picks that language's caption track when the channel published one, and tells Whisper what to expect.

## `allowWhisperFallback` (type: `boolean`):

On (default): Shorts without captions are transcribed with Whisper speech-to-text, charged at the higher `asr_transcript` rate. Off: those videos return a `CAPTIONS_UNAVAILABLE` error and cost you nothing — the cheapest way to run large batches, and a reasonable choice on YouTube, where most talking content is captioned already.

## `forceWhisper` (type: `boolean`):

Off (default): YouTube's caption track is used whenever there is one. On: every Short is transcribed with Whisper and charged at the `asr_transcript` rate — 20× the caption price. Worth it only when you have judged YouTube's auto-captions too poor for your use.

## `maxDurationSeconds` (type: `integer`):

Videos longer than this are skipped with a `DURATION_EXCEEDED` error and are not charged. A Short is at most 180 s, so the 600 s default only matters if you paste a regular YouTube video by mistake — which it will happily transcribe from captions.

## `maxAsrDurationSeconds` (type: `integer`):

Applies only to videos with no captions. Whisper costs roughly a second of CPU per 1.5 seconds of audio, so a per-video price only works up to a point; past this length a caption-less video returns `ASR_DURATION_EXCEEDED` and costs nothing. The default of 180 s is exactly the length of the longest Short. Videos **with** captions are never subject to this — they run to the full `maxDurationSeconds`.

## `maxVideos` (type: `integer`):

Safety cap on how many URLs from the list are processed.

## `whisperModel` (type: `string`):

Accuracy vs speed for the Whisper fallback only — videos with captions ignore this. `base` is the recommended balance.

## `beamSize` (type: `integer`):

Decoding beam width for the Whisper fallback. 1 is fastest; higher is marginally more accurate and slower.

## `concurrency` (type: `integer`):

How many videos to resolve at once. Whisper transcription is always serialised regardless of this setting, to keep the run inside its memory limit.

## `includeFailedItems` (type: `boolean`):

On (default): unavailable, private, age-restricted, too-long and non-YouTube URLs get a dataset row carrying an `error` code, so you can reconcile inputs to outputs. Failed rows are never charged.

## `useResidentialFallback` (type: `boolean`):

On (default), and you should leave it on. YouTube answers datacenter addresses — including Apify Proxy's and the platform's own — with "Sign in to confirm you're not a bot", for videos that are perfectly public. This retries those requests through Apify's residential proxy, which is the route that works. With this off, most Shorts return `LOGIN_REQUIRED`. Only requests the cheap route already refused use residential traffic, and caption downloads stay on the cheap route.

## `proxyConfiguration` (type: `object`):

Proxy used for the first attempt on every request. The run pins one proxy session, because YouTube binds a video's audio URL to the IP that fetched the player response.

## Actor input object example

```json
{
  "videoUrls": [
    "https://www.youtube.com/shorts/BDf5mOfSkkw",
    "https://www.youtube.com/shorts/pM62yIJNuao"
  ],
  "allowWhisperFallback": true,
  "forceWhisper": false,
  "maxDurationSeconds": 600,
  "maxAsrDurationSeconds": 180,
  "maxVideos": 1000,
  "whisperModel": "base",
  "beamSize": 1,
  "concurrency": 5,
  "includeFailedItems": true,
  "useResidentialFallback": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

Every transcribed Short, including rows for the ones that failed.

# 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/shorts/BDf5mOfSkkw",
        "https://www.youtube.com/shorts/pM62yIJNuao"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("sauliusautomatesit/youtube-shorts-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/shorts/BDf5mOfSkkw",
        "https://www.youtube.com/shorts/pM62yIJNuao",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("sauliusautomatesit/youtube-shorts-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/shorts/BDf5mOfSkkw",
    "https://www.youtube.com/shorts/pM62yIJNuao"
  ]
}' |
apify call sauliusautomatesit/youtube-shorts-transcript-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,sauliusautomatesit/youtube-shorts-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/cRcWzciy3PzO3T7bd/builds/xyiKXZUg9zI6jE98Y/openapi.json
