# YouTube Shorts Scraper - Views, Likes & Channel Data (`eiv/youtube-shorts-scraper`) Actor

Scrape every Short from any YouTube channel: title, view count, thumbnail, duration, publish date and description, with exact view and like counts on request. Takes handles, channel ids or URLs. No login, no API key, no quota.

- **URL**: https://apify.com/eiv/youtube-shorts-scraper.md
- **Developed by:** [Eimantas V](https://apify.com/eiv) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 short scrapes

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 Scraper

Scrape every Short from any YouTube channel — **by handle, channel id or URL** — with view counts, thumbnails, titles and publish order. No Google account, no API key, and none of the 10,000-unit daily quota the official YouTube Data API imposes.

One row per Short, plus a summary row per channel that tells you exactly what was and was not collected.

***

### What you get on every Short

| | |
|---|---|
| **Identity** | video id, canonical `youtube.com/shorts/…` link, title |
| **Reach** | view count as a number *and* as YouTube words it, plus a flag saying which kind you got |
| **Image** | thumbnail at full 1080×1920, not a resized preview |
| **Order** | `rank` — its position in the channel's Shorts tab, newest first |
| **Channel** | channel id, display name and URL on every row, so nothing needs joining back together |

Turn on **exact statistics** and each Short also carries its **exact view count**, **duration in seconds**, **UTC publish date** and **full description**. Turn on **like counts** as well and you get the exact number of likes.

***

### The rounded-number problem, and what this does about it

A channel's Shorts tab does not contain view counts. It contains *prose about* view counts — `"12M views"` — and 12M is anything from 12,000,000 to 12,999,999. That is a range a million wide, presented as a fact.

Most exports quietly turn `"12M views"` into `12000000` and hand it to you as a number. It looks exact. It is a rounding artefact, and it will happily tell you two Shorts performed identically when one has 400,000 more views than the other.

This Actor does three things instead:

1. Keeps **`viewCountText`** — the original `"12M views"` — so the source figure is never lost.
2. Puts the expanded number in **`viewCount`**, which is genuinely useful for sorting and thresholds.
3. Marks every row with **`viewCountIsExact`**, so you always know which you are holding.

Switch on **exact view counts** and `viewCount` is replaced with the real figure and the flag flips to `true`. Measured on a MrBeast Short listed as `"12M views"`: the exact count was **12,574,516**. The rounding hid over half a million views.

The exact figure is read from whichever of two sources answers, so a bot check on one of them costs you the duration and the publish date but never the view count itself.

**Like counts** work the same way, except there is no rounded version to fall back on — YouTube publishes no like count anywhere on a Shorts listing, so this is the only way to get one at all.

***

### Point it at a channel however you already write it

- `@MrBeast` or just `MrBeast`
- `UCX6OQ3DkcsbYNE6H8uQQuVA`
- `https://www.youtube.com/@MrBeast`
- `https://www.youtube.com/channel/UCX6OQ3DkcsbYNE6H8uQQuVA`
- the older `/c/Name` and `/user/Name` forms

A channel id costs nothing to resolve; a handle costs one extra request. Paste a `/watch` or `/shorts` link — a single video rather than a channel — and you get a clear bad-input row saying so, rather than a silent guess at which channel you meant.

Every run is deduplicated **across the whole run**, so listing the same channel twice under two different names collects it once and bills it once.

***

### An empty result always tells you why

A channel that has never posted a Short returns a perfectly successful, completely empty response — identical in shape to what a broken scraper produces. So every channel gets a summary row that says which it was:

| `zeroReason` | What it means |
|---|---|
| `channel-has-no-shorts` | The channel was read successfully and genuinely posts no Shorts |
| `all-shorts-filtered-out` | Shorts were found; **your minimum-views filter** removed all of them |
| `charge-limit-reached` | Your charge limit stopped the run before this channel |
| `run-short-limit-reached` | The total-Shorts ceiling was reached first |
| `run-timeout-reached` | The run stopped itself early to finish cleanly rather than be killed |

The same row carries `stoppedOn`, `pagesFetched`, `shortsScanned` and `shortsFiltered`, so a short result is never ambiguous. If a channel stopped because it ran out of page budget rather than out of Shorts, `stoppedOnScanLimit` says so and the log tells you which setting to raise.

A channel handle that does not exist produces an **error row** with `errorClass: channel-not-found` — never an empty result that could be mistaken for a quiet channel.

***

### Honest about limits

- **YouTube's Shorts tab is finite.** It pages back a long way but not forever, and a channel with tens of thousands of Shorts will stop before you reach the end. `stoppedOn` tells you when that happens.
- **Listing view counts are rounded.** Covered above — the flag is there so you always know.
- **Duration, publish date and description are best-effort.** YouTube applies a bot check to one of the two requests behind exact statistics, based on the IP address a run happens to be given. **Exact view counts are unaffected** — they are read from a second source that is not gated — but those three fields come only from the checked request and are left `null` on the Shorts it refuses. Measured on a full run: exact views on 100% of rows, the other three on roughly half. The summary row's `detailsGated` is the count, so an empty cell always has a number explaining it, and **you are never charged for statistics you did not receive**. Re-running, or using a different proxy country, normally returns a different set.
- **The description is never approximated.** The lighter source that supplies view counts also carries a description, but it truncates every URL inside it — `https://www.themo…` instead of the real link. Rather than hand you a broken link that looks like a working one, the description is left empty unless the full version was retrieved.
- **Like counts are occasionally withheld** by YouTube on individual Shorts. Those rows get `likeCount: null`, and **a null is never charged for**.
- **Comments are not included.** They are a different surface with a different cost, and bundling them would mean charging every customer for something most do not want.
- **Sorting is YouTube's.** The Shorts tab is newest-first and there is no "most viewed" ordering available on it, so `rank` reflects recency. Sort by `viewCount` after the fact.

***

### Pricing

Pay per result. No monthly platform rental on top.

| Event | Price per 1,000 | What triggers it |
|---|---|---|
| **Short scraped** | **$2.00** | One Short written to your dataset |
| **Exact statistics** | **$1.00** | Optional — exact views, duration, publish date, description |
| **Exact like count** | **$1.20** | Optional — one exact like count actually obtained |

Prices fall with your Apify plan, down to **$1.20 / $0.60 / $0.80** per 1,000 on Gold.

A listing-only run — titles, rounded views, thumbnails, ranks — costs **$2.00 per 1,000 Shorts and nothing else**. The two optional events each cost one extra request *per Short*, which is why they are priced separately rather than folded into one headline number you would pay whether you used them or not.

**What is never charged:**

- Shorts removed by your minimum-views filter — they are never written, so they are never billed
- The same Short seen twice in one run
- Summary rows and error rows
- A request that failed and had to be retried
- A like count that came back empty

***

### Getting started

1. Put one or more channels in **YouTube channels** — handles, ids or URLs, one per line.
2. Set **Max Shorts per channel**. YouTube returns 48 per request, so 100 is three requests and takes seconds.
3. Leave the exact-statistics options **off** for a first run to see the shape of the data cheaply. Turn them on when you need real numbers.
4. Run it. Results appear in the dataset as they are collected — the **Shorts** view is one row per Short, the **Run report** view is the per-channel summary.

Leave the channel list empty and it runs a small demo on `@MrBeast`, so you can see the output format before spending anything meaningful.

# Actor input Schema

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

Channels whose Shorts you want, one per line. Accepts a handle (@MrBeast or just MrBeast), a channel id (UCX6OQ3DkcsbYNE6H8uQQuVA), or any channel URL, including the older /c/ and /user/ forms. A handle costs one extra request to resolve; a UC… id costs none. A /watch or /shorts link is a single video rather than a channel and is reported as bad input instead of being guessed at. Leave empty to run a small demo on @MrBeast.

## `includeDetails` (type: `boolean`):

Off by default. The Shorts tab only ever says "12M views", which is anything from 12,000,000 to 12,999,999. Turning this on fetches each Short individually and replaces the rounded figure with the exact one (measured: 12,576,545 where the list said 12M), and adds the duration in seconds, the publish date and the full description. Every row says which kind of view count it got: viewCountIsExact. YouTube bot-checks the request that carries duration, publish date and description, based on the IP address a run happens to be given, so those three can come back empty on some Shorts - measured on a full run, roughly half. Exact view counts are read from a second source and arrived on every row. Each summary row reports how many Shorts were affected under detailsGated, and you are never charged for statistics that were not returned. Switching on like counts below makes exact view counts more reliable still, at no extra request.

## `includeLikes` (type: `boolean`):

Off by default, and only does anything when the option above is on. YouTube no longer publishes exact like counts anywhere on a Shorts listing, so each one needs its own request to the watch page - a much heavier response than the exact-views request. Charged per like count actually obtained: a Short whose count could not be read is left null and is not charged for it. That same response also states the exact view count, so turning this on makes exact view counts more reliable at no extra request and no extra charge.

## `minViews` (type: `integer`):

Skip Shorts below this view count. Filtered Shorts are never written and never charged. With exact statistics off this is compared against YouTube's rounded figure, so a threshold of 1,000,000 against a Short listed as "1M views" is approximate; with exact statistics on it is compared against the exact number instead.

## `maxShortsPerChannel` (type: `integer`):

Stop after this many Shorts from each channel. YouTube returns 48 per request, so 100 is three requests.

## `maxTotalShorts` (type: `integer`):

A ceiling across every channel in the run, so one prolific channel cannot spend the whole budget. Counted on rows actually written, and a Short that appears under two of your inputs is only counted, written and charged once.

## `maxPagesPerChannel` (type: `integer`):

A work limit rather than an output limit: pages READ, not rows kept. It matters when a strict Minimum views filter drops nearly everything, where without it a run could read a whole channel to find ten rows. At 48 Shorts per page the default reaches roughly 960. If a channel stops on this limit the summary row says so, so it is never mistaken for the channel running out of Shorts.

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

How many requests may be in flight at once. Raising this does not speed up a run on its own, because the delay below paces requests globally rather than per worker.

## `requestDelayMs` (type: `integer`):

Milliseconds between the START of one request and the next, counted across the whole run rather than per worker. YouTube was measured to accept about 174 requests a minute before it starts refusing; the default is 150 a minute, comfortably under that. Lowering it risks blocks that cost retries.

## `proxyConfig` (type: `object`):

Datacenter proxy is the default and is what this Actor was measured on. YouTube's internal API needs no login, no API key and no browser here, so there is nothing for a residential proxy to unblock — it only makes the run slower and heavier. Use one only if you specifically need results as they appear from another country.

## Actor input object example

```json
{
  "channels": [
    "@MrBeast"
  ],
  "includeDetails": false,
  "includeLikes": false,
  "minViews": 0,
  "maxShortsPerChannel": 100,
  "maxTotalShorts": 1000,
  "maxPagesPerChannel": 20,
  "maxConcurrency": 5,
  "requestDelayMs": 400,
  "proxyConfig": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `dataset` (type: `string`):

One row per Short with its title, view count, thumbnail, publish position and channel, plus exact view and like counts when those options are on. Every channel also gets a summary row saying how many Shorts were read, how many were filtered out, and why the run stopped.

# 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": [
        "@MrBeast"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("eiv/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": ["@MrBeast"] }

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

```

## MCP server setup

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