# YouTube Shorts Scraper - Exact Views, Dates & Channels (`neverempty/youtube-shorts-scraper`) Actor

For influencer vetting, sponsorship reporting and trend research: Shorts from any YouTube channel or search term with id, URL, title, channel, thumbnail and the exact view count. The number printed under a Short is truncated, not rounded - one shown as 10M really had 10,962,867 on 2026-09-08.

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

## Pricing

from $2.92 / 1,000 short returneds

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 - views, dates and channels

**For creator and competitor analysis, trend research and tracking Shorts performance**: Shorts from any YouTube channel or search term — id, URL, title, channel, thumbnail and view count.

**The part that makes this one different is uncomfortable to say out loud: the number YouTube prints under a Short is truncated, not rounded, so it is always too low.** `10M views` is not 10,000,000 and it is not "about 10 million" either. Measured on 8 September 2026 on one channel's eight most recent Shorts:

| What the Shorts list says | What it really was | Missing |
|---|---|---|
| `10M views` | **10,962,867** | 962,867 |
| `24M views` | 24,936,936 | 936,936 |
| `41M views` | 41,717,006 | 717,006 |
| `47M views` | 47,213,481 | 213,481 |
| `156M views` | 156,150,868 | 150,868 |

**In 8 of 8 cases the listed number was the lower one**, and the first row of that table is in `test/fixtures/` with the test suite asserting the **962,867** difference, so you can recount it yourself.

**So you get all four values and a flag**: `viewCountText` (`10M views`), `viewCountApprox` (`10000000`), `viewCount` and **`viewCountIsExact`**. Turn on **Exact numbers** and it fetches the real count for one extra request per Short — measured from Apify on 8 September 2026, **10 of 10** came back exact, and `10M views` was **10,969,868**.

**About the publish date, measured rather than promised:** the Shorts list carries no date at all. With exact numbers on, YouTube gives this Actor either a full timestamp (`publishedAt`, `2026-08-23T09:00:04-07:00`) or, when it will not serve the video endpoint to the address the run comes from, only the day (`publishedDateText`, `Aug 23, 2026`). Runs from Apify's own servers currently get the day; turning the proxy on gets the timestamp. Rows carry whichever one is true, and **never a made-up time**.

No API key, no login. Export as JSON, CSV or Excel.

***

### What every row carries

- **The Short**: `shortId`, `url`, `title`, `thumbnailUrl`, `accessibilityText`
- **The numbers**: `viewCountText` / `viewCountApprox` / `viewCount` / `viewCountIsExact`
- **Only with Exact numbers**: `publishedAt` (a full timestamp) or `publishedDateText` (the day, when
  that is all YouTube will give this run), and `lengthSeconds` when it comes with them
- **The channel**: `channelName`, `channelId`, `channelHandle`, `channelUrl`
- **Where it came from**: `channelInput` or `query`, plus `page` and `rank`

***

### The four things this Actor refuses to fake

**1. A truncated number is never presented as exact.** Every row says which it is, and a free row in
each run says how many rows are still approximate and what that costs you.

**2. It does not invent a publish date.** The Shorts list carries no date at all - not even "3 days
ago". Rows carry `publishedAt: null` until you ask for exact numbers, rather than a guess.

**3. A channel with no Shorts is not a channel that could not be read.** Those are two different free
rows (`no-shorts` and `channel-not-found`), because "this creator posts no Shorts" and "we could not
reach YouTube" mean opposite things to whoever reads your spreadsheet.

**4. It follows the right continuation.** A channel's Shorts tab hands back three "next page" tokens
and only one of them is the real one - taking the last, which is what the neighbouring endpoint needs,
returns an empty second page. This Actor tries the candidates and keeps the one that actually produces
Shorts it has not seen, and never returns the same Short twice.

***

### Example output (one row, shortened)

```json
{
  "channelInput": "@MrBeast",
  "shortId": "5mU6SRS2Bxo",
  "url": "https://www.youtube.com/shorts/5mU6SRS2Bxo",
  "title": "World’s Largest Tennis Match",
  "viewCountText": "10M views",
  "viewCountApprox": 10000000,
  "viewCount": 10962867,
  "viewCountIsExact": true,
  "publishedAt": "2026-08-23T09:00:04-07:00",
  "publishedDateText": "Aug 23, 2026",
  "lengthSeconds": 36,
  "channelName": "MrBeast",
  "channelId": "UCX6OQ3DkcsbYNE6H8uQQuVA",
  "channelHandle": "@MrBeast",
  "channelUrl": "https://www.youtube.com/channel/UCX6OQ3DkcsbYNE6H8uQQuVA",
  "page": 1,
  "rank": 1
}
```

Without **Exact numbers** the same row carries `viewCount: 10000000`, `viewCountIsExact: false`,
`publishedAt: null` and `lengthSeconds: null` - and costs one request per 48 Shorts instead of one
request each.

***

### Input

- `channels` - handles such as `@MrBeast`, channel URLs, or ids starting with `UC`. Only when
  `channels`, `searchQueries` and `shortUrls` are all empty does the run use the example channel
  `@MrBeast`, and a free `example-input` row says so. Nothing is ever added to what you asked for.
  If the form's example `@MrBeast` is still in `channels` when you give your own search terms or
  Shorts, it is skipped (not fetched, not charged) and a free `example-input-skipped` row says so.
- `searchQueries` - words or hashtags. YouTube search mixes Shorts with ordinary videos; only the
  Shorts are kept, and a free row says so when a search returned results but no Shorts.
- `shortUrls` - specific Shorts by URL or id. These always come back with exact numbers.
- `maxShortsPerSource` - how many Shorts per channel or search term (a channel page holds about 48).
- `maxRecords` - the most rows this run may return and charge for. When the cap bites, rows are taken
  **evenly from every channel and search term**, so none of them disappears completely.
- `exactNumbers` - fetch the real view count, and the publish date at whatever precision YouTube gives
  this run, at one request per Short. If YouTube refuses that request entirely, one free row says so and
  the rows keep the truncated count, still marked as not exact.
- `minViews` - without exact numbers this compares against the truncated count, which is lower than
  the real one, so nothing that really passes your threshold is dropped.
- `maxLengthSeconds`, `keywords`, `keywordMatch`, `excludeKeywords` - Shorts whose length is unknown
  are **kept**, not thrown away.
- `monitoringMode`, `resetMonitoringState` - remembers the Shorts already returned to you and returns
  only new ones. Run it on a schedule to watch a channel and pay only for what is new.
- `maxRequests` - hard ceiling on requests to YouTube, counting channel lookups, list pages and the
  extra request per Short that exact numbers need.
- `useProxy` - off by default. Measured 8 September 2026: a plain connection returned HTTP 200.

***

### Rows you are never charged for

Every row that is not a Short carries a reason and the words `This row is not charged`:

| `status` | when |
|---|---|
| `no-shorts` | the channel was found and its Shorts tab was empty |
| `channel-not-found` | YouTube says that handle or id does not exist |
| `invalid-input` | something you passed is not a YouTube channel or Short |
| `no-results` | a search returned results but none of them was a Short |
| `approximate-views` | how many rows still carry the truncated count, and what to do about it |
| `unreadable` | the answer could not be read |
| `bot-check` | YouTube answered with a bot check |
| `no-match` | Shorts were read but none matched your filters |
| `request-limit-reached` | the run stopped at your `maxRequests` |
| `maximum-reached` | rows were left out by your `maxRecords` |
| `exact-numbers-unavailable` | exact numbers were asked for but YouTube refused them to this run |
| `no-new-since-last-run` | monitoring mode: everything read had already been returned to you |
| `budget-reached` | the run hit the maximum total charge set for it |

***

### How the numbers in this README can be checked

The responses are in `test/fixtures/`, taken from production on 8 September 2026:

```bash
npm test        # checks against those real responses, including the 962,867 difference above
npm run mutate  # deliberate breakages; every one of them must turn the checks red
```

# Actor input Schema

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

Whose Shorts you want. Give handles such as @MrBeast, channel URLs, or channel ids starting with UC. Anything that is not a YouTube channel comes back as its own free row instead of being silently dropped. If channels, search terms and individual Shorts are all left empty, the example channel @MrBeast is used and a free row says so. If the example @MrBeast is still here when you give your own search terms or Shorts, it is skipped and not charged.

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

Words or hashtags to search. YouTube search returns Shorts and ordinary videos mixed together; only the Shorts are kept, and a free row says so when a search returned results but no Shorts.

## `shortUrls` (type: `array`):

Specific Shorts by URL or 11-character id. These are always fetched with their exact view count and publish date.

## `maxShortsPerSource` (type: `integer`):

How many Shorts to collect for each channel or search term. YouTube serves about 48 per page from a channel and this Actor follows the pages for you.

## `maxRecords` (type: `integer`):

The most rows this run may return and charge for. When the limit cuts the result, rows are taken evenly from every channel and search term, so none of them disappears completely, and a free row says what was left out.

## `exactNumbers` (type: `boolean`):

The Shorts list only carries a truncated view count such as "10M views" and no date at all. Turn this on to fetch the real number and the publish date for every Short, at one extra request each. Measured on 8 September 2026, the listed number was the lower one in 8 of 8 cases and "10M views" was really 10,969,868. Depending on the address the run comes from, the date arrives either as a full timestamp or as the day only - rows say which, and never invent a time.

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

Keep only Shorts with at least this many views. Without "Exact numbers" this compares against the truncated count YouTube shows, which is always lower than the real one, so nothing that really passes your threshold is thrown away. 0 turns the filter off.

## `maxLengthSeconds` (type: `integer`):

Keep only Shorts no longer than this. Length is only known when "Exact numbers" is on; Shorts whose length is unknown are kept rather than thrown away. 0 turns the filter off.

## `keywords` (type: `array`):

Case-insensitive words matched against the title and the channel name of the Shorts this run read. Leave empty to keep everything.

## `keywordMatch` (type: `string`):

any keeps a Short that contains at least one of the words. all requires every word.

## `excludeKeywords` (type: `array`):

Case-insensitive. A Short whose title or channel name contains any of these words is dropped before it is charged.

## `monitoringMode` (type: `boolean`):

Remembers the Shorts already returned to you and returns only the ones it has not. Run it on a schedule to watch a channel and pay only for what is new.

## `resetMonitoringState` (type: `boolean`):

Clears that memory once, so the next monitoring run treats every Short as new.

## `maxRequests` (type: `integer`):

A hard ceiling on how many requests this run may make to YouTube, counting channel lookups, list pages and the extra request per Short that exact numbers need.

## `useProxy` (type: `boolean`):

Off by default: measured on 8 September 2026, a plain connection returned HTTP 200. Turn it on only if you see bot-check rows - it is slower and costs proxy traffic.

## Actor input object example

```json
{
  "channels": [
    "@MrBeast"
  ],
  "searchQueries": [],
  "shortUrls": [],
  "maxShortsPerSource": 50,
  "maxRecords": 100,
  "exactNumbers": false,
  "minViews": 0,
  "maxLengthSeconds": 0,
  "keywords": [],
  "keywordMatch": "any",
  "excludeKeywords": [],
  "monitoringMode": false,
  "resetMonitoringState": false,
  "maxRequests": 60,
  "useProxy": false
}
```

# Actor output Schema

## `results` (type: `string`):

One row per Short: its id and URL, the title, the channel with its id, handle and URL, the thumbnail, and the view count both as YouTube writes it in the Shorts list and as a number, together with a flag saying whether that number is exact. YouTube truncates the listed count and shows no date at all, so the exact view count, the real publish timestamp and the length are filled in only when you ask for them, at one extra request per Short. A channel that does not exist, a channel with no Shorts, a search that returned no Shorts, a bot check, an unreadable answer, filters that matched nothing, rows left out by your maximum and a monitoring run with nothing new each come back as their own row and are not charged.

# 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"
    ],
    "searchQueries": [],
    "shortUrls": [],
    "maxShortsPerSource": 50,
    "maxRecords": 100,
    "minViews": 0,
    "maxLengthSeconds": 0,
    "keywords": [],
    "keywordMatch": "any",
    "excludeKeywords": [],
    "maxRequests": 60
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/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"],
    "searchQueries": [],
    "shortUrls": [],
    "maxShortsPerSource": 50,
    "maxRecords": 100,
    "minViews": 0,
    "maxLengthSeconds": 0,
    "keywords": [],
    "keywordMatch": "any",
    "excludeKeywords": [],
    "maxRequests": 60,
}

# Run the Actor and wait for it to finish
run = client.actor("neverempty/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"
  ],
  "searchQueries": [],
  "shortUrls": [],
  "maxShortsPerSource": 50,
  "maxRecords": 100,
  "minViews": 0,
  "maxLengthSeconds": 0,
  "keywords": [],
  "keywordMatch": "any",
  "excludeKeywords": [],
  "maxRequests": 60
}' |
apify call neverempty/youtube-shorts-scraper --silent --output-dataset

```

## MCP server setup

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