# YouTube Shorts Scraper - $0.40 per 1,000 Shorts (`dami_studio/youtube-shorts-scraper`) Actor

Scrape YouTube Shorts from any channel or keyword. One row per Short: title, video id, URL, exact view count, exact like count, publish date, duration, thumbnail, channel name and id. Newest first. The cheapest YouTube Shorts scraper on the market. No API key, no cookies, no browser.

- **URL**: https://apify.com/dami\_studio/youtube-shorts-scraper.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** Videos, Social media
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.40 / 1,000 short scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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

Give it YouTube channel URLs or handles and get that channel's Shorts feed back, newest first — or give it a keyword and get the Shorts that match it. Every row carries the title, the video id, the Short URL, the exact view count, the exact like count, the publish date, the duration in seconds, the thumbnail and the channel name and id. No account, no cookies, no API key, no browser.

- Shorts specifically — read from the channel's own Shorts tab, not filtered out of the uploads feed by guessing at duration.
- Exact numbers, not the rounded ones the page shows: 27,549,434 views and 1,023,955 likes, not "27M" and "1M".
- Channels and keywords in the same run, up to 20 targets, with the row budget shared evenly between them.
- @handles, /channel/UC…, /c/… , /user/… and legacy vanity URLs all resolve, including the old redirect-style ones.
- Newest first by real publish date, not by whatever order the feed happened to serve.
- Runs with empty input return a labelled sample row, free, so you can see the shape before spending anything.

### Price

**$0.40 per 1,000 Shorts**, plus a **$0.0005 start fee per run** (billed per gigabyte of run memory, so exactly that on the default 1 GB).

This is **the cheapest YouTube Shorts scraper on the market**, and it is the same rate on every plan, free or paid. There are no volume tiers, no minimum spend, no subscription and no add-on fees. What you read here is what you pay on day one and on day four hundred.

| Shorts | Total cost |
|---|---|
| 100 | $0.0405 |
| 1,000 | $0.4005 |
| 10,000 | $4.0005 |
| 100,000 | $40.0005 |

#### What is actually charged

- **One `short-scraped` event per Short row written to the dataset.** Nothing else is metered per row.
- **Free:** the sample row an empty run returns, and every diagnostic row — a blocked target, a dead URL, a search that matched nothing. Those rows all carry `"charged": false`.
- Shorts already returned earlier in the same run are dropped before they are charged, so overlapping keywords never bill twice.
- A channel that has never posted a Short costs you nothing beyond the start fee — you get one free diagnostic row saying so.
- Ordinary long-form videos that YouTube mixes into a filtered search are discarded, not billed as Shorts.
- A run that finds nothing costs the start fee and nothing more.
- Rows never leave the dataset without a charge, and are never charged without a row. The billed event is a named one, so there is no price quietly attached to `apify-default-dataset-item` — the trick that makes some scrapers bill you for their own error messages.

### Input

```json
{
  "channelUrls": [
    "https://www.youtube.com/@MrBeast",
    "@mkbhd"
  ],
  "searchQueries": [
    "cooking hacks"
  ],
  "maxItems": 60,
  "sortBy": "newest"
}
```

| Field | What it does |
|---|---|
| `channelUrls` | Channel URLs or handles. `https://www.youtube.com/@MrBeast`, `@mkbhd`, `https://www.youtube.com/channel/UCX6OQ3DkcsbYNE6H8uQQuVA`, `youtube.com/c/mkbhd`, `youtube.com/user/PewDiePie` and a bare `UC…` id all work. Up to 20 targets per run when combined with `searchQueries`. |
| `searchQueries` | Keywords to search YouTube Shorts for. One row per matching Short. Combine with `channelUrls` in the same run if you like. |
| `maxItems` | Total rows across all targets. The budget is split evenly, so four targets and 60 rows gives you fifteen each. Default 20, hard ceiling 5,000. Keep it low while testing — you pay per row. |
| `maxItemsPerTarget` | Optional hard cap per channel or keyword. Leave it empty to let `maxItems` split evenly. |
| `includeStats` | On by default. Fetches each Short's exact view count, like count, publish date and duration. Turn it off for a faster run that returns only what the feed itself shows: title, id, URL, thumbnail and a rounded view count. |
| `sortBy` | `newest` (default) orders the rows this run collected by their real publish date. `feed` keeps YouTube's own order instead. |
| `proxyUrls` | Leave empty. Fill it in only if you want the traffic to leave through proxy servers you already pay for, as `http://user:pass@host:port`. |

Run it with **empty input** and you get one clearly labelled sample row, free, so you can see the output shape before you spend anything.

### Output

One row per Short. A real row from a real run:

```json
{
  "ok": true,
  "charged": true,
  "recordType": "short",
  "input": "https://www.youtube.com/@MrBeast",
  "searchQuery": null,
  "videoId": "LiH-P4rSkLI",
  "title": "Can You Pass This Classroom Quiz?",
  "url": "https://www.youtube.com/shorts/LiH-P4rSkLI",
  "viewCount": 27549434,
  "viewCountText": "27M views",
  "viewCountIsApproximate": false,
  "likeCount": 1023955,
  "publishedAt": "2026-08-11T16:00:06.000Z",
  "publishedTimeText": null,
  "durationSeconds": 74,
  "thumbnail": "https://i.ytimg.com/vi/LiH-P4rSkLI/sardefault.jpg",
  "channelName": "MrBeast",
  "channelId": "UCX6OQ3DkcsbYNE6H8uQQuVA",
  "channelUrl": "https://www.youtube.com/channel/UCX6OQ3DkcsbYNE6H8uQQuVA",
  "position": 1,
  "statsError": null,
  "scrapedAt": "2026-08-15T21:52:26.584Z"
}
```

#### Field notes

- `videoId` — the 11-character YouTube id. Stable, and safe to use as a primary key across re-runs.
- `url` — always the `/shorts/<id>` form. The same video also plays at `/watch?v=<id>` if you prefer that.
- `viewCount` — the exact integer when `includeStats` is on. With stats off it falls back to the rounded figure the feed prints.
- `viewCountIsApproximate` — true when the count is the rounded feed figure rather than the exact one. Check this before doing arithmetic on views.
- `viewCountText` — the view count exactly as the feed shows it, e.g. "27M views". Kept so you can reconcile a row against what you see on screen.
- `likeCount` — the exact like count. Null when the creator has hidden likes on that Short — around 2 in 100 in testing.
- `publishedAt` — publish time as ISO 8601 in UTC.
- `publishedTimeText` — the relative date a search result carries, e.g. "13 days ago". Usually null on channel rows, where `publishedAt` is exact.
- `durationSeconds` — length in whole seconds. Shorts run up to about three minutes, so this is not always under 60.
- `thumbnail` — the largest still the feed offered. It is a vertical 9:16 frame, not the 16:9 thumbnail a normal video has.
- `channelId` — the `UC…` id, which never changes even when the handle does. `channelUrl` is built from it.
- `position` — the row's place in the delivered order for its target, starting at 1.
- `statsError` — set only when the per-Short metadata call could not complete for that one video; the row is still delivered with everything the feed gave.

Every real row carries `"charged": true`. Sample rows carry `"_sample": true` and diagnostic rows carry `"_diagnostic": true` with an `errorCode` you can filter on, and neither is ever billed.

### How it works

- Shorts live behind their own tab on a channel and their own filter in search, so the run reads those surfaces directly rather than pulling the uploads feed and guessing which entries are Shorts by their length. Guessing gets it wrong both ways: it misses Shorts longer than a minute and it picks up ordinary short videos that are not Shorts at all.
- Everything is plain JSON, requested and parsed. No page rendering, no headless browser, no login, no cookies and no API key — which is why a run costs what it does.
- A second small request per Short is what turns "27M views" into 27,549,434, and adds the exact like count, the publish date and the duration. It is the smallest response that carries all four; the obvious Shorts-specific alternative returns ninety times more data for less information.
- Requests leave through a large pool of rotating datacenter addresses, so a per-address rate limit is answered by moving rather than by waiting.
- Channel handles are resolved in cost order: an id already in the URL costs nothing, a handle costs one small lookup, and the old redirect-style vanity URLs cost one more. Only if all of that fails does the run fall back to reading the channel page itself.

### What people use it for

- Tracking a creator's Shorts output over time — schedule the same channels daily and diff on `videoId` to see only what is new, with view and like counts attached.
- Competitive research on a niche: run five channels in one go and sort the rows by `viewCount` to see which Shorts actually landed.
- Finding hooks that work — pull a keyword's Shorts, sort by views per day since `publishedAt`, and read the top twenty titles before writing your own.
- Building a Shorts dataset for analysis: the rows are flat and typed, so they load straight into a spreadsheet or a database with no cleaning.
- Influencer vetting — a channel's recent Shorts with real like-to-view ratios tell you more about engagement than a subscriber count does.
- Feeding a dashboard or a newsletter with a clean table instead of scraping a rendered page.

### Reading the output

Every run writes three kinds of row, and they are easy to tell apart:

- **Real rows** carry `"charged": true` and `"recordType": "short"`. These are the rows you paid for, one billed event each.
- **The sample row** carries `"_sample": true` and `"charged": false`. There is exactly one, it only appears when the input had no channels and no keywords, and it exists so you can look at the output shape before you spend anything.
- **Diagnostic rows** carry `"_diagnostic": true`, `"charged": false` and an `errorCode` you can switch on: `NO_RESULTS` when a channel has no Shorts or a keyword matched none, `NOT_FOUND` when a handle does not exist, `RATE_LIMITED` when YouTube throttled the run, `SERVER_ERROR` when YouTube itself faltered, `NETWORK` when it could not be reached, and `TIME_BUDGET` when the run ran out of time before reaching a target. Each carries a plain-English `error` and the `input` it belongs to.

If you only want the data, filter on `charged == true`. The count of those rows always equals the number of events you were billed for, so the dataset is its own invoice.

### Channels versus keywords

The two modes behave differently, and it is worth knowing which you want.

**A channel** returns that channel's Shorts tab, which is already in newest-first order, and pages through it until your row budget runs out. Ask for 500 and you get the 500 most recent Shorts that channel posted.

**A keyword** returns the Shorts YouTube considers the best matches for that phrase. YouTube will not sort a Shorts-filtered search by date — asking it to do so quietly turns the Shorts filter off and hands back ordinary long videos instead — so `sortBy: "newest"` orders the Shorts this run actually collected by their real publish date. That is "the newest of the matches found", not "the newest Shorts on YouTube matching this phrase". If you want the latter, run a channel instead: a creator's tab really is chronological.

### Limitations

- A channel that has never posted a Short has no Shorts tab at all. You get one free diagnostic row saying so, and no charged rows. Around one channel in twelve in our test set was like this.
- `likeCount` is null when the creator has hidden likes on a Short. That is roughly 2 rows in 100 and there is no way to recover the number.
- Keyword search returns what YouTube ranks as the best matches, not an exhaustive index. Two runs of the same keyword minutes apart can return a slightly different set, and the row budget decides how deep it goes.
- Keyword results cannot be sorted newest-first by YouTube itself, so `sortBy: "newest"` sorts the Shorts the run collected. Channel mode has no such caveat.
- View and like counts are a snapshot at read time. A Short that is currently trending will read differently ten minutes later.
- Comment counts, descriptions, hashtags, the audio track and the transcript are not returned. This Actor is the Shorts feed with its headline metrics, nothing more.
- Age-restricted and members-only Shorts may return without a like count or a publish date; the row still arrives with everything the feed showed, and `statsError` says why.
- Private, deleted and region-blocked Shorts never appear in the feed in the first place, so they cannot be returned.
- Ordering is by publish date, not by views. Sort the dataset yourself if you want the biggest rather than the newest.
- The hard ceilings are 5,000 rows and 20 targets per run. Split larger jobs across runs.
- Everything is read logged out, so anything YouTube only shows to a signed-in viewer is out of scope by design.

### Questions

**How is this different from scraping a channel's videos and keeping the short ones?**

Shorts are a separate surface on YouTube, not a length category. A channel's uploads feed and its Shorts tab are different lists, and a Short can run up to about three minutes while plenty of ordinary videos are under a minute. Filtering by duration therefore misses real Shorts and invents fake ones. This Actor reads the Shorts surfaces themselves.

**Why are some like counts empty?**

The creator turned like counts off for that video. It is a per-video setting and the number is simply not published anywhere, so no scraper can recover it. You still get the row and everything else about it.

**Do I need a proxy, an API key or a YouTube account?**

None of the three. The run brings its own egress and its cost is already inside the price you see. The `proxyUrls` field exists only for callers who specifically want traffic to leave through servers they already own.

**Will the run fail if a channel does not exist?**

No. You get one uncharged diagnostic row for that target and the run carries on to the others, finishing as succeeded. A failed run would still bill the start fee, which would mean paying to be told about a typo.

**Can I run this on a schedule?**

Yes. Nothing is held between runs, so the same input is safe to repeat. Use `videoId` to work out which rows are new since last time.

**What does turning `includeStats` off actually save?**

It skips one small request per Short, so the run finishes noticeably faster. You keep the title, id, URL, thumbnail, channel and a rounded view count, and you lose the exact view count, the like count, the publish date and the duration. The price per row is the same either way.

**How do I get exactly the rows I paid for?**

Filter the dataset on `"charged": true`. Sample and diagnostic rows are always `false`, and the number of charged rows always equals the number of billed events.

# Actor input Schema

## `channelUrls` (type: `array`):

YouTube channels to pull Shorts from, newest first. Any format works: https://www.youtube.com/@MrBeast, @mkbhd, https://www.youtube.com/channel/UCX6OQ3DkcsbYNE6H8uQQuVA, youtube.com/c/mkbhd, youtube.com/user/PewDiePie, or a bare UC... id. Up to 20 targets per run in total, counting keywords.

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

Search YouTube Shorts for these words or phrases. One row per matching Short. You can mix keywords and channels in the same run.

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

Total number of Shorts to return across all channels and keywords. The budget is split evenly between the targets, so four targets and 60 rows gives you fifteen of each. Keep it low while you are testing - you pay per row.

## `maxItemsPerTarget` (type: `integer`):

Hard cap on how many Shorts to take from each channel or keyword. Leave it empty and the maximum-rows budget is simply split evenly between your targets.

## `includeStats` (type: `boolean`):

On by default. Adds one small request per Short to turn the rounded "27M views" the feed shows into the exact 27,549,434, and to add the exact like count, the publish date and the duration in seconds. Turn it off for a faster run that returns only what the feed itself carries. The price per row is the same either way.

## `sortBy` (type: `string`):

"Newest first" orders the Shorts this run collected by their real publish date. "Feed order" keeps the order YouTube served them in.

## `proxyUrls` (type: `array`):

Leave this empty. By default the run rotates a large pool of addresses that cost you nothing per gigabyte. Fill it in only if you specifically want the traffic to leave through proxy servers you already pay for, in the form http://user:pass@host:port.

## Actor input object example

```json
{
  "channelUrls": [
    "https://www.youtube.com/@MrBeast"
  ],
  "maxItems": 20,
  "includeStats": true,
  "sortBy": "newest"
}
```

# Actor output Schema

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

Every row in the default dataset: videoId, title, url, viewCount, likeCount, publishedAt, publishedTimeText, durationSeconds, thumbnail, channelName, channelId, channelUrl, viewCountText, position, input. An empty, blocked or unmatched run returns a single uncharged row explaining what happened instead.

# 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 = {
    "channelUrls": [
        "https://www.youtube.com/@MrBeast"
    ],
    "maxItems": 20,
    "includeStats": true,
    "sortBy": "newest"
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/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 = {
    "channelUrls": ["https://www.youtube.com/@MrBeast"],
    "maxItems": 20,
    "includeStats": True,
    "sortBy": "newest",
}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/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 '{
  "channelUrls": [
    "https://www.youtube.com/@MrBeast"
  ],
  "maxItems": 20,
  "includeStats": true,
  "sortBy": "newest"
}' |
apify call dami_studio/youtube-shorts-scraper --silent --output-dataset

```

## MCP server setup

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