# YouTube Shorts Scraper (`upadhyay.labs/youtube-shorts-scraper`) Actor

Scrape YouTube Shorts from channels, search queries, and direct URLs. Extract video metadata, engagement metrics, subtitles, translations, and detailed channel information.

- **URL**: https://apify.com/upadhyay.labs/youtube-shorts-scraper.md
- **Developed by:** [Upadhyay Labs](https://apify.com/upadhyay.labs) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.90 / 1,000 shorts

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 Shorts Scraper

Extract structured data from YouTube Shorts at scale — titles, view counts, likes, durations, hashtags, upload dates, captions, and full channel statistics — from **any channel, search query, or direct Shorts URL**. No browser required, no YouTube API key, no daily quota.

> Try it instantly: paste `mkbhd` into the **Channels** field and click **Start**.

***

### What does YouTube Shorts Scraper do?

YouTube Shorts Scraper collects metadata from public YouTube Shorts without a browser or a YouTube Data API key. It reads the same public data YouTube's own web pages render and paginates through channel Shorts tabs and search results — the same mechanism the "load more" button uses on the site.

**Each output row contains:**

- Video metadata: title, URL, thumbnail, view count, likes, duration, upload date, description, hashtags, links, comment count, age-restriction status, paid-promotion flag, members-only flag, collaborators
- Subtitles / captions: full SRT text for configured languages (auto-generated or user-submitted tracks)
- Channel details: name, handle, subscriber count, verification badge, avatar, banner, description, join date, total views and videos, country, social links
- Provenance: which input triggered this result, what URL was actually scraped, position in the listing, timestamp

**Supported input sources:**

| Source | Example |
|---|---|
| Channel handle | `mkbhd`, `@NASA` |
| Channel ID | `UCBcRF18a7Qf58cCRy5xuWwQ` |
| Channel URL | `https://www.youtube.com/@mkbhd` |
| Search query | `"satisfying ASMR"` |
| Direct Short URL | `https://www.youtube.com/shorts/VIDEO_ID` |

***

### Why use YouTube Shorts Scraper?

Building this on the Apify platform instead of a custom script gets you:

- **No YouTube Data API key or quota** — the official API caps Shorts-relevant metadata and burns daily quota fast; this Actor reads the same public data YouTube's own web client renders, with no key and no 10,000-unit/day ceiling.
- **Lightweight and fast** — no headless browser required, so runs finish 5–10× faster and use a fraction of the memory browser-based scrapers need for the same volume.
- **Built-in proxy rotation & session management** — residential [Apify Proxy](https://docs.apify.com/proxy) integration and automatic session retirement on blocks, so you don't hand-roll IP rotation.
- **Pay only for results** — [Pay-Per-Event pricing](#how-much-will-it-cost-to-scrape-youtube-shorts) charges per Short actually delivered, not per minute of compute.
- **Scheduling, webhooks, and API access** — run on a cron schedule, trigger a webhook on completion, or call it from your own code via the [Apify API](https://docs.apify.com/api/v2) or [JS/Python clients](#3-run-via-apify-javascript-or-python-client).
- **Structured, typed output** — every field has a documented type and a defined `null` meaning (never a guess), so downstream pipelines don't have to special-case missing data.

***

### Use cases

- **Creator analytics** — track posting frequency, engagement trends, and format evolution across any creator's Shorts library
- **Market research** — benchmark your channel's like-to-view ratio and comment rate against competitors in the same niche
- **Influencer discovery** — find active Shorts creators by topic, then filter by subscriber count and engagement rate
- **Content monitoring** — watch a brand's or competitor's Shorts feed for new uploads, hashtags, or paid promotions
- **Caption and transcription pipelines** — pull auto-generated or user-submitted SRT captions at scale for translation, summarization, or keyword extraction
- **SEO and hashtag research** — identify which hashtags appear most on high-performing Shorts in a niche
- **Academic and social-media research** — build datasets of short-form video engagement across channels or topics

***

### How to scrape YouTube Shorts

#### 1. Run on the Apify platform (no setup needed)

1. Open the Actor on [Apify Store](https://apify.com/store) and click **Try for free**.
2. In the **Input** tab, add at least one **Channel**, **Search query**, or **Direct URL**.
3. Adjust limits and enrichment options (see [Input](#input) below).
4. Click **Start** and wait for the run to finish.
5. Download results from the **Dataset** tab in JSON, CSV, or Excel.

#### 2. Run via the Apify API

Start a run and download results programmatically:

```bash
## Start a run
curl -X POST \
  "https://api.apify.com/v2/acts/YOUR_ACTOR_ID/runs?token=YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "channels": ["mkbhd"],
    "maxResultsShorts": 50,
    "extendedVideoDetails": true,
    "extendedChannelDetails": true
  }'
```

See the [Apify API docs](https://docs.apify.com/api/v2) for full run management, webhooks, and dataset export endpoints.

#### 3. Run via Apify JavaScript or Python client

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

const client = new ApifyClient({ token: 'YOUR_API_TOKEN' });

const run = await client.actor('YOUR_ACTOR_ID').call({
    channels: ['mkbhd', 'nasa'],
    maxResultsShorts: 100,
    extendedVideoDetails: true,
    extendedChannelDetails: true,
    subtitleLanguages: ['en'],
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_API_TOKEN")

run = client.actor("YOUR_ACTOR_ID").call(run_input={
    "channels": ["mkbhd", "nasa"],
    "maxResultsShorts": 100,
    "extendedVideoDetails": True,
    "extendedChannelDetails": True,
    "subtitleLanguages": ["en"],
})

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)
```

***

### How much will it cost to scrape YouTube Shorts?

This Actor uses **Pay-Per-Event (PPE)** pricing — you pay a flat, predictable fee for each Short actually written to the dataset, never for platform compute time, proxy bandwidth, or failed/retried requests.

| Event | Charged? |
|---|---|
| Short successfully scraped and saved | ✅ Yes — one event per Short |
| Error / placeholder row (no Shorts found, unavailable video, empty search) | ❌ No |

**Cost control:** set `maxItems` or `maxResultsShorts` to cap output. The run also stops automatically once your configured budget limit (`ACTOR_MAX_TOTAL_CHARGE_USD`) is reached — you are never billed beyond what you approved.

See the **Pricing** tab on this Actor's Store page for the current per-event rate.

***

### Input

At least one of `channels`, `searchQueries`, or `startUrls` is required. All other fields are optional.

#### Sources

| Field | Type | Default | Description |
|---|---|---|---|
| `channels` | `string[]` | — | Channel handles, `@handles`, channel IDs, or channel/Shorts URLs. Examples: `"mkbhd"`, `"@NASA"`, `"UCBcRF18a7Qf58cCRy5xuWwQ"`, `"https://www.youtube.com/@mkbhd"` |
| `searchQueries` | `string[]` | `[]` | Plain-text search terms. Results come from YouTube's Shorts search tab. |
| `startUrls` | `RequestSource[]` | `[]` | Direct `https://www.youtube.com/shorts/VIDEO_ID` links scraped individually, bypassing channel discovery. |

##### Accepted channel formats

| Input | Resolves to |
|---|---|
| `"nasa"` / `"@NASA"` | `/@NASA/shorts` |
| `"UCBcRF18a7Qf58cCRy5xuWwQ"` | `/channel/UCBcRF18a7Qf58cCRy5xuWwQ/shorts` |
| `"https://www.youtube.com/@NASA/videos"` | `/@NASA/shorts` (tab switched automatically) |
| `"https://www.youtube.com/c/nasa"` | `/c/nasa/shorts` |

Duplicate inputs (e.g. `"nasa"` and `"@NASA"`) are automatically deduplicated by resolved URL.

#### Limits & filtering

| Field | Type | Default | Description |
|---|---|---|---|
| `maxResultsShorts` | `integer` | `25` | Maximum Shorts to collect per channel or search query. Range: 1–20,000. |
| `maxItems` | `integer` | `0` | Global cap across the entire run. `0` = unlimited (bounded only by `maxResultsShorts` per source). |
| `oldestPostDate` | `string` | — | Stop paginating a source once Shorts older than this date are encountered. Accepts ISO dates (`"2024-01-01"`) or relative values (`"7 days"`, `"2 days ago"`). When set, `sortChannelShortsBy` is automatically forced to `NEWEST`. |
| `sortChannelShortsBy` | `string` | `"NEWEST"` | Order for channel Shorts pages: `NEWEST` (most recent first), `POPULAR` (most viewed first), `OLDEST` (oldest first). Ignored for search queries. |

#### Data enrichment

| Field | Type | Default | Description |
|---|---|---|---|
| `extendedChannelDetails` | `boolean` | `true` | Fetches the channel About page for description, join date, location, total views, total videos, avatar, and banner. One extra request per unique channel. |
| `extendedVideoDetails` | `boolean` | `true` | Visits each Short's watch page for exact like count, comment count, exact upload date, description, subtitle tracks, collaborators, and paid-promotion flag. ~2× slower but significantly richer data. |
| `subtitleLanguages` | `string[]` | `["en"]` | BCP-47 language codes for which to download and populate full SRT caption text. Other tracks appear in output with `srt: null` (no extra request made). Set `[]` to skip all SRT downloads. Only applies when `extendedVideoDetails` is `true`. |
| `translationLanguage` | `string` | `""` | BCP-47 code (e.g. `"es"`, `"fr"`). When set, re-fetches the watch page with `?hl=<lang>` to get a localized title and description. `translatedTitle` and `translatedText` are `null` when no genuine translation exists. One extra request per video. Only applies when `extendedVideoDetails` is `true`. |

#### Performance & proxy

| Field | Type | Default | Description |
|---|---|---|---|
| `proxyConfiguration` | `object` | Apify RESIDENTIAL | Proxy settings. Residential proxies are strongly recommended — YouTube throttles datacenter IPs quickly. |
| `maxRequestRetries` | `integer` | `6` | Maximum retries per failed request before recording an error row. Range: 0–20. |
| `maxConcurrency` | `integer` | `15` | Parallel requests. Higher = faster, but YouTube throttles aggressive concurrency even through residential proxies. Avoid exceeding ~20 without also widening your proxy pool. |

#### Example input — MKBHD's latest 50 Shorts with English captions

```json
{
    "channels": ["mkbhd"],
    "maxResultsShorts": 50,
    "sortChannelShortsBy": "NEWEST",
    "extendedChannelDetails": true,
    "extendedVideoDetails": true,
    "subtitleLanguages": ["en"],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": ["RESIDENTIAL"]
    }
}
```

***

### Output

One dataset row per Short. Below is a real sample record from MKBHD's channel (`mkbhd`), with `extendedVideoDetails: true` and `extendedChannelDetails: true`:

```json
{
    "title": "This can SEE Sound!",
    "type": "shorts",
    "id": "ai1gIWacf0I",
    "url": "https://www.youtube.com/shorts/ai1gIWacf0I",
    "thumbnailUrl": "https://i.ytimg.com/vi/ai1gIWacf0I/maxresdefault.jpg",
    "viewCount": 2213234,
    "date": "2026-10-27T20:14:55.000Z",
    "likes": 63749,
    "durationSeconds": 90,
    "duration": "00:01:30",
    "likesToViewsRatio": 0.0288,
    "commentsCount": 1294,
    "commentsTurnedOff": false,
    "text": "This is an acoustic imaging camera",
    "descriptionLinks": [],
    "hashtags": [],
    "subtitles": [
        {
            "language": "English (auto-generated)",
            "languageCode": "en",
            "type": "asr",
            "url": "https://www.youtube.com/api/timedtext?v=ai1gIWacf0I&...",
            "isTranslatable": true,
            "srt": null
        }
    ],
    "isMembersOnly": null,
    "isAgeRestricted": false,
    "isMonetized": null,
    "isPaidContent": false,
    "collaborators": [],
    "location": null,
    "translatedTitle": null,
    "translatedText": null,
    "aiVideoDescription": null,
    "aiVideoSummary": null,
    "transcriptionUrl": null,
    "channelName": "Marques Brownlee",
    "channelUrl": "https://www.youtube.com/channel/UCBJycsmduvYEL83R_U4JriQ",
    "channelId": "UCBJycsmduvYEL83R_U4JriQ",
    "channelUsername": "mkbhd",
    "numberOfSubscribers": 21400000,
    "isChannelVerified": true,
    "channelAvatarUrl": "https://yt3.googleusercontent.com/...",
    "channelBannerUrl": "https://yt3.googleusercontent.com/...",
    "channelDescription": "MKBHD: Quality Tech Videos | YouTuber | Geek | Consumer Electronics...",
    "channelJoinedDate": "Mar 21, 2008",
    "channelTotalViews": 5847901236,
    "channelTotalVideos": 1863,
    "channelLocation": "United States",
    "channelDescriptionLinks": [
        { "text": "Twitter", "url": "http://twitter.com/MKBHD" },
        { "text": "Instagram", "url": "http://instagram.com/MKBHD" }
    ],
    "aboutChannelInfo": {
        "channelName": "Marques Brownlee",
        "channelUrl": "https://www.youtube.com/channel/UCBJycsmduvYEL83R_U4JriQ",
        "channelId": "UCBJycsmduvYEL83R_U4JriQ",
        "channelUsername": "mkbhd",
        "channelAvatarUrl": "https://yt3.googleusercontent.com/...",
        "channelBannerUrl": "https://yt3.googleusercontent.com/...",
        "channelDescription": "MKBHD: Quality Tech Videos | YouTuber | Geek...",
        "channelJoinedDate": "Mar 21, 2008",
        "channelTotalViews": 5847901236,
        "channelTotalVideos": 1863,
        "channelLocation": "United States",
        "channelDescriptionLinks": [
            { "text": "Twitter", "url": "http://twitter.com/MKBHD" }
        ],
        "numberOfSubscribers": 21400000
    },
    "inputChannelUrl": "https://www.youtube.com/@mkbhd",
    "input": "mkbhd",
    "fromYTUrl": "https://www.youtube.com/@mkbhd/shorts",
    "fromChannelListPage": "shorts",
    "order": 0,
    "scrapedAt": "2026-10-30T16:26:56.338Z"
}
```

***

### Output field reference

#### Video fields

| Field | Type | Notes |
|---|---|---|
| `title` | `string\|null` | Short's title |
| `translatedTitle` | `string\|null` | Populated only when `translationLanguage` is set **and** YouTube has a genuine translation; `null` otherwise |
| `translatedText` | `string\|null` | Same gate as `translatedTitle` |
| `type` | `string` | Always `"shorts"` |
| `id` | `string` | YouTube video ID |
| `url` | `string` | `https://www.youtube.com/shorts/{id}` |
| `thumbnailUrl` | `string\|null` | `maxresdefault.jpg` (1280×720) from YouTube CDN |
| `viewCount` | `integer\|null` | Total view count |
| `date` | `string\|null` | ISO 8601 UTC upload timestamp; requires `extendedVideoDetails: true` |
| `likes` | `integer\|null` | Like count; requires `extendedVideoDetails: true` |
| `durationSeconds` | `integer\|null` | Duration in whole seconds; requires `extendedVideoDetails: true` |
| `duration` | `string\|null` | `HH:MM:SS` formatted duration; requires `extendedVideoDetails: true` |
| `likesToViewsRatio` | `number\|null` | `round(likes / viewCount, 4)`; `null` when either value is missing or `viewCount` is 0 |
| `commentsCount` | `integer\|null` | From engagement panel or InnerTube `/next`; `null` when indeterminate (not the same as comments being disabled) |
| `commentsTurnedOff` | `boolean\|null` | `true` = explicitly disabled (confirmed signal), `false` = explicitly enabled (confirmed signal), `null` = could not determine |
| `text` | `string\|null` | Short's description |
| `descriptionLinks` | `array` | Links extracted from the description |
| `hashtags` | `array` | `#tags` extracted from title and description |
| `subtitles` | `array\|null` | Caption track objects; `null` when no tracks exist. Each object always includes an `srt` key. |
| `isMembersOnly` | `boolean\|null` | `null` when no members-only badge is detectable |
| `isAgeRestricted` | `boolean\|null` | `true` = explicit age gate; `false` = `isFamilySafe` confirmed; `null` = no reliable signal (common on Shorts) |
| `isMonetized` | `null` | Always `null` — YouTube does not expose monetisation status in public data. Kept for schema compatibility. |
| `isPaidContent` | `boolean` | `true` only when YouTube's "Includes paid promotion" disclosure overlay is present; requires `extendedVideoDetails: true` |
| `collaborators` | `array` | `@-mentions` extracted from the description |
| `location` | `string\|null` | This video's location tag (not the channel's country); `null` for virtually all Shorts |
| `aiVideoDescription` | `null` | Not implemented. Kept for schema compatibility. |
| `aiVideoSummary` | `null` | Not implemented. Kept for schema compatibility. |
| `transcriptionUrl` | `null` | Not implemented. Use `subtitles[]` for caption data. |

#### Subtitle tracks — `subtitles[]`

Each object in the `subtitles` array:

| Field | Type | Notes |
|---|---|---|
| `language` | `string` | Human-readable language name (e.g. `"English"`, `"English (auto-generated)"`) |
| `languageCode` | `string` | BCP-47 code (e.g. `"en"`) |
| `type` | `string` | `"user_generated"` or `"asr"` (auto-generated) |
| `url` | `string` | YouTube timedtext API URL — always present |
| `isTranslatable` | `boolean` | Whether YouTube offers translation of this track |
| `srt` | `string\|null` | Full SRT caption text when this track's language code matches `subtitleLanguages`; `null` otherwise (no extra request made) |

SRT format: sequential 1-based index, `HH:MM:SS,mmm --> HH:MM:SS,mmm` timestamps, decoded text (no HTML entities).

#### Channel fields (top-level, always flat)

| Field | Type | Notes |
|---|---|---|
| `channelName` | `string\|null` | Channel display name |
| `channelUrl` | `string\|null` | Canonical `/channel/UCxxxxxx` URL |
| `channelId` | `string\|null` | `UCxxxxxx` identifier |
| `channelUsername` | `string\|null` | Handle without `@` |
| `numberOfSubscribers` | `integer\|null` | Subscriber count |
| `isChannelVerified` | `boolean\|null` | Verified badge presence |
| `channelAvatarUrl` | `string\|null` | Channel profile image URL |
| `channelBannerUrl` | `string\|null` | Channel banner image URL |
| `channelDescription` | `string\|null` | Requires `extendedChannelDetails: true` |
| `channelJoinedDate` | `string\|null` | Requires `extendedChannelDetails: true` |
| `channelTotalViews` | `integer\|null` | Requires `extendedChannelDetails: true` |
| `channelTotalVideos` | `integer\|null` | Requires `extendedChannelDetails: true` |
| `channelLocation` | `string\|null` | Channel's declared country; requires `extendedChannelDetails: true` |
| `channelDescriptionLinks` | `array` | Social links from the channel About page; requires `extendedChannelDetails: true` |
| `aboutChannelInfo` | `object\|null` | Self-contained nested object with all channel fields above. `null` when `extendedChannelDetails: false` or fetch failed. |

#### Provenance fields

| Field | Notes |
|---|---|
| `inputChannelUrl` | Resolved `@handle` URL (e.g. `https://www.youtube.com/@mkbhd`). `null` for search sources. |
| `input` | The raw value exactly as given in the input, unresolved. |
| `fromYTUrl` | Actual URL scraped to produce this record. |
| `fromChannelListPage` | `"shorts"` for channel sources; `null` for search queries. |
| `order` | 0-based position in the source listing. |
| `scrapedAt` | ISO 8601 UTC timestamp of when this record was saved. |

***

### Error rows

When an input cannot be processed, the Actor pushes a structured error row to the dataset instead of crashing the run. Error rows are **not** charged as events.

```json
{
    "url": "https://www.youtube.com/@nonexistentchannel",
    "input": "nonexistentchannel",
    "error": "CHANNEL_DOES_NOT_EXIST",
    "note": "Channel handle resolved to a 404.",
    "scrapedAt": "2026-10-30T16:26:56.338Z"
}
```

#### Error codes

| Error code | Cause |
|---|---|
| `CHANNEL_DOES_NOT_EXIST` | The handle or URL does not resolve to a valid channel |
| `CHANNEL_HAS_NO_SHORTS` | Channel exists and is accessible, but has no public Shorts |
| `NOT_FOUND` | HTTP 404 — page does not exist |
| `PARSE_FAILED` | Page loaded but `ytInitialData` JSON could not be read (YouTube layout change) |
| `VIDEO_UNAVAILABLE` | Video is deleted, private, or region-blocked |
| `AGE_RESTRICTED` | Video requires a logged-in account to view |
| `NO_RESULTS` | Search query returned no Shorts |
| `DATE_FILTER_TOO_STRICT` | Shorts exist on the channel but none are newer than `oldestPostDate` |
| `REQUEST_FAILED` | All retries exhausted (network error, persistent blocking) |

***

### Integrations

YouTube Shorts Scraper connects to the tools you already use — no extra code required.

#### Zapier

Trigger a Zapier Zap when a run finishes and automatically push Shorts data to Google Sheets, Airtable, Slack, or any of Zapier's 5,000+ apps. See [Apify + Zapier integration](https://apify.com/integrations/zapier).

#### Make (formerly Integromat)

Use the Apify Make module to start runs on a schedule and route results into your Make scenarios. See [Apify + Make integration](https://apify.com/integrations/make).

#### Google Sheets

Export any completed dataset directly to a Google Sheet from the Apify Console in one click, or automate it via the Apify Google Sheets integration.

#### Webhooks

Configure a webhook on the Actor run to POST the dataset URL to your own endpoint the moment the run succeeds. Go to **Actor → Settings → Webhooks** in the Apify Console.

#### Apify API

Every run, dataset, and key-value store is accessible via the [Apify REST API v2](https://docs.apify.com/api/v2). Paginate results, stream NDJSON, or poll run status programmatically.

#### MCP Server (AI agents)

This Actor is callable as a tool by any MCP-compatible AI agent (Claude, Cursor, etc.) via the [Apify MCP Server](https://docs.apify.com/platform/integrations/mcp). No additional setup — the Actor appears as a native tool once MCP is connected.

#### Scheduled runs

Set the Actor to run on a daily, weekly, or custom cron schedule from **Actor → Settings → Schedules** in the Apify Console. Combined with webhooks or Google Sheets export, this creates a fully automated YouTube Shorts monitoring pipeline.

***

### Limitations

- **Subtitle SRT may be `null`** even for a language listed in `subtitleLanguages`. YouTube's PoToken (proof-of-origin token) requirement can block automated caption retrieval on individual videos — this is a platform-wide restriction affecting all automated scrapers and cannot be reliably bypassed. The track's `url` field is always still returned regardless.
- **`location`** is `null` for virtually all Shorts. It only populates for content derived from live broadcasts that carry a location tag.
- **`isMonetized`** is always `null`. YouTube does not expose monetisation status in public InnerTube data.
- **`commentsCount`** may be `null` even when comments are enabled. This happens when the engagement panel header omits the count — it is not the same as `commentsTurnedOff: true`.
- **`commentsTurnedOff`** is three-state: `true` only on a confirmed explicit disabled signal; `false` on a confirmed enabled signal; `null` when neither signal is found.
- **`isAgeRestricted`** is `null` for most Shorts because YouTube omits the `isFamilySafe` flag on Shorts pages. It is not `false` by default.
- **CSV / Excel export** — nested objects (`aboutChannelInfo`) and arrays (`subtitles`, `collaborators`, `descriptionLinks`, `channelDescriptionLinks`) are JSON-serialized into a single column when exported via the Apify Console. This is standard Apify flattening behavior. Use JSON or JSONL export for full fidelity.

***

### FAQ

#### Do I need a YouTube API key or Google Cloud project?

No. YouTube Shorts Scraper reads the same public data YouTube's own web pages display — no API key, no OAuth, no daily quota.

#### Is it legal to scrape YouTube Shorts?

This Actor only extracts data that is already publicly visible on YouTube — titles, view counts, captions, and channel stats the uploader chose to publish. It does not access private data, bypass logins, or scrape anything behind authentication. See the disclaimer below for your own compliance responsibilities.

#### Why is `commentsTurnedOff` sometimes `null` instead of `true` or `false`?

YouTube doesn't always expose an explicit "comments enabled/disabled" signal on the Shorts player response. Rather than guess, this Actor returns `null` whenever neither a confirmed-enabled nor confirmed-disabled signal is found — a `false` positive here (wrongly reporting comments as off) is worse for downstream automation than an honest "unknown."

#### Why is `srt` sometimes `null` even though I set `subtitleLanguages`?

YouTube intermittently gates its caption/transcript endpoint behind a PoToken (proof-of-origin token) requirement — a platform-wide restriction, not an Actor bug. When that happens for a given run, the subtitle track's metadata (language, URL, type) is still returned; only the downloaded SRT text is `null`. See [Limitations](#limitations).

#### How many Shorts can I scrape per run?

There's no hard limit from the Actor itself — set `maxResultsShorts` (per source) and `maxItems` (global cap) as high as you need. In practice, run size is bounded by your proxy pool and your Pay-Per-Event budget (`ACTOR_MAX_TOTAL_CHARGE_USD`), both of which you control from the input and run settings.

#### Can I scrape Shorts by hashtag or topic instead of a channel?

Yes — use `searchQueries` with your hashtag or keyword (e.g. `"#satisfying"` or `"AI news"`). Results come from YouTube's Shorts-filtered search, the same listing the Shorts search tab shows.

#### Does this Actor work without residential proxies?

It can run with any Apify Proxy group or your own proxy URLs, but YouTube throttles and blocks datacenter IPs aggressively. **Residential proxies are strongly recommended** for reliable runs at any meaningful volume.

***

### Disclaimer

This Actor is intended for ethical, legitimate data collection and does not extract any private user data such as emails, exact personal locations, or anything not already published by the channel owner or uploader — it only extracts what is **publicly visible on YouTube**. You are responsible for how you use the extracted data: personal data (even when publicly posted) may be protected under GDPR in the EU and under similar regulations elsewhere. If you intend to process or store personal data at scale, consult your own legal counsel to confirm your use case is compliant.

***

### Support

- **Bug report or feature request:** open an issue on the **Issues** tab of this Actor's Store page.
- **Custom scraping solution:** contact the developer through their Apify profile.

***

*Made with ☕ by **upadhyay.labs***

# Actor input Schema

## `channels` (type: `array`):

YouTube channel handles, @handles, channel URLs or /shorts URLs. Examples: "nasa", "@nasa", "https://www.youtube.com/@nasa".

## `searchQueries` (type: `array`):

Plain-text keywords to search Shorts for. Alternative or addition to channels.

## `startUrls` (type: `array`):

Direct https://www.youtube.com/shorts/VIDEO_ID links to scrape individually, bypassing channel discovery.

## `maxResultsShorts` (type: `integer`):

Maximum number of Shorts to collect from each channel or search query. Even the largest channels rarely exceed a few thousand Shorts, so this is capped at 20,000 to keep the field meaningful.

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

Global cap across the entire run. 0 = unlimited (bounded only by maxResultsShorts per source).

## `oldestPostDate` (type: `string`):

Stop paginating a source once Shorts older than this date are reached. Accepts ISO dates ("2024-01-01") or relative values ("7 days"). When set, sortChannelShortsBy is automatically set to NEWEST.

## `sortChannelShortsBy` (type: `string`):

Order in which Shorts are returned from channel pages. NEWEST = most recent first (default), POPULAR = most viewed first, OLDEST = oldest first. Ignored for search queries.

## `extendedChannelDetails` (type: `boolean`):

Adds subscriber count, channel description, join date, location, total views/videos, avatar/banner. Makes one extra request per unique channel.

## `extendedVideoDetails` (type: `boolean`):

Visits each Short's watch page for exact like count, comment count, description, subtitle tracks, and collaborators. Approximately 2× slower but much richer data.

## `subtitleLanguages` (type: `array`):

BCP-47 language codes for which to attempt full SRT subtitle text. Other tracks appear in the output with URL-only (srt: null). Only applies when extendedVideoDetails is true. Note: YouTube currently restricts automated caption/transcript retrieval on some videos (a platform-wide PoToken requirement, not specific to this Actor) -- when that happens srt stays null for the configured language too, but the subtitle track's URL is always still returned. Default: \["en"].

## `translationLanguage` (type: `string`):

BCP-47 language code (e.g. "es", "fr"). When set, re-fetches the watch page with ?hl=<lang> and records translatedTitle / translatedText when YouTube returns a real translation (i.e. when the localized values differ from the originals). Both fields are null when no translation exists. One extra request per video. Only applies when extendedVideoDetails is true.

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

Residential proxies strongly recommended – YouTube blocks datacenter IPs quickly.

## `maxRequestRetries` (type: `integer`):

Maximum number of times to retry a failed request before recording it as an error.

## `maxConcurrency` (type: `integer`):

Higher = faster, but YouTube throttles aggressive concurrency even through residential proxies. 15 is a safe default with real throughput headroom; avoid going past ~20 without also widening your proxy pool.

## Actor input object example

```json
{
  "channels": [
    "mkbhd"
  ],
  "searchQueries": [],
  "startUrls": [],
  "maxResultsShorts": 25,
  "maxItems": 0,
  "sortChannelShortsBy": "NEWEST",
  "extendedChannelDetails": true,
  "extendedVideoDetails": true,
  "subtitleLanguages": [
    "en"
  ],
  "translationLanguage": "",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "maxRequestRetries": 6,
  "maxConcurrency": 15
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

# 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 = {
    "channels": [
        "mkbhd"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("upadhyay.labs/youtube-shorts-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 = { "channels": ["mkbhd"] }

# Run the Actor and wait for it to finish
run = client.actor("upadhyay.labs/youtube-shorts-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 '{
  "channels": [
    "mkbhd"
  ]
}' |
apify call upadhyay.labs/youtube-shorts-scraper --silent --output-dataset

```

## MCP server setup

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