# YouTube Channel & Video Scraper (`eiv/youtube-channel-scraper`) Actor

Scrape YouTube channels and videos without a browser or API key: subscriber counts, full upload lists, exact view counts, durations, publish dates, keywords, and optional likes and comments. No quota limits. From $0.30 per 1,000 videos.

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

## Pricing

from $0.30 / 1,000 video scrapes

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 Channel & Video Scraper

Channel stats and full video lists with **exact view counts** — no browser, no API key, no quota.

```
@mkbhd   Marques Brownlee   21.1M subscribers · 1.8K videos
         6 recent uploads · median 3,642,713 views · uploading every 7 days

  Google Pixel 11/Pro/Fold Impressions      1,562,071 views   11:14   14 hours ago
  Galaxy Z Fold 8 Review: Honeymoon's Over  4,097,984 views   11:35   12 days ago
  Framework 13 Pro: The Modular Laptop      3,187,441 views   12:48   2 weeks ago
```

**One channel with 6 videos: 8 requests, 0.4 MB, 0.8 seconds.**

***

### Why it is fast and cheap

Most YouTube scrapers load the page in a headless browser. This one calls the same internal API youtube.com calls from your browser, and the difference is not small: a watch page is **1.25 MB of HTML wrapped around 16 KB of data**. Reading the data directly makes runs roughly eighty times lighter, which is why the price below is what it is.

***

### What you get

**Per channel** — `channelId`, `handle`, `title`, `description`, `keywords`, `subscriberCount`, `videoCount`, `avatarUrl`, `rssUrl`, plus rollups over the videos returned: `medianViewsScraped` and **`uploadIntervalDays`**, the mean gap between uploads.

**Per video**

| Group | Fields |
|---|---|
| **Reach** | `viewCount`, **`viewCountIsExact`**, **`viewsPerDay`**, `viewCountText` |
| **Engagement** | `likeCount` (exact), `commentCount`, **`likesPerThousandViews`** — opt-in |
| **Timing** | `publishedAt`, `publishedText`, `daysSincePublished` |
| **Content** | `title`, `descriptionSnippet`, `keywords`, `category`, `durationSeconds` |
| **Format** | `isShort`, `isLive`, `thumbnailUrl` |

`viewsPerDay` is the one that matters for comparison. A three-year-old video with 4M views and a three-week-old video with 4M views are not the same result, and raw view counts cannot tell you which is winning.

***

### Who this is for

- **Influencer marketing** — vet creators before you pay them. Subscriber count is vanity; `medianViewsScraped` against `subscriberCount` shows how many people actually watch, and `uploadIntervalDays` shows whether the channel is still active.
- **Content strategy** — pull a competitor's last 200 uploads, sort by `viewsPerDay`, and read what worked.
- **Trend research** — `keywords` and `category` across a set of channels, refreshed daily.
- **Media monitoring** — watch specific channels for new uploads with `publishedWithinDays: 1`.

Pairs with the TikTok Creator Scraper: same buyer, the other half of the picture.

***

### Input

```json
{
  "channels": ["@mkbhd", "@MrBeast"],
  "maxVideosPerChannel": 100,
  "publishedWithinDays": 90
}
```

Accepts handles, channel URLs, `UC…` ids, legacy `/c/` and `/user/` paths, and — for `videos` — watch, `youtu.be`, `/shorts/` and `/live/` links.

| Option | Default | Notes |
|---|---|---|
| `maxVideosPerChannel` | `50` | Your main billing guard. `0` = channel stats only |
| `includeShorts` | `false` | Separate tab. Shorts carry no duration or date of their own — those need `exactVideoStats` |
| `publishedWithinDays` | — | Applied after fetching; YouTube's tab has no date filter |
| `exactVideoStats` | `true` | Exact views and timestamps; one small request per video |
| `includeEngagement` | `false` | Likes and comments — **~25× the data** |
| `includeDescription` | `false` | Off keeps the dataset far smaller |
| `language` / `country` | `en` / `US` | Affects display text and region-restricted videos |

***

### Four things worth knowing

Each was measured against live channels.

**Subscriber counts are rounded, and that is YouTube's doing.** The site itself publishes "21.1M subscribers" — the exact number has not been public since 2019. `subscriberCount` is that text parsed to `21100000`, and `subscriberCountText` keeps the original so you can see it is rounded. Any tool quoting an exact subscriber figure is inventing precision.

**View counts are exact, but only if you ask — and only if YouTube agrees.** A channel listing gives "1.5M views"; resolving the video gives `1584034`. `exactVideoStats` is on by default and costs one 16 KB request per video.

YouTube sometimes answers that request without describing the video, which datacenter addresses see routinely. When it does the Actor tries a second client, and if that is also refused it keeps the rounded listing figure rather than inventing one.

Two fields tell you exactly what you got, and they are deliberately separate:

- **`viewCountIsExact`** — whether `viewCount` is the real number. It can be true even when the video lookup failed, because turning on engagement recovers the exact count from that payload.
- **`publishedText`** — when it is set, `publishedAt` was estimated from wording like "2 weeks ago". Videos the lookup did describe have an exact timestamp and no `publishedText`.

`keywords` and `category` come only from the video lookup, so they are empty when it is refused. The run log reports both gaps separately.

**Likes cost 25× more than everything else.** The like total is not in the cheap endpoint — fetching it means a second request that is roughly 400 KB against 16 KB. That is why `includeEngagement` is off by default. Comment counts are rounded by YouTube itself: "2.4M" is as precise as the site is.

**Publish dates are exact where possible, approximate otherwise.** With `exactVideoStats` you get a real timestamp. Without it, "2 weeks ago" is anchored to the coarsest unit YouTube gave, and `publishedText` always carries the original wording so you can tell which you are looking at.

***

### Output

```json
{
  "recordType": "video",
  "videoId": "Z6z_feacXW8",
  "channelTitle": "Marques Brownlee",
  "title": "Galaxy Z Fold 8 Review: Honeymoon's Over",
  "viewCount": 4097984, "viewCountText": "4M views",
  "durationSeconds": 695, "durationText": "11:35",
  "publishedAt": "2026-08-01T…", "publishedText": "12 days ago",
  "daysSincePublished": 12, "viewsPerDay": 341499,
  "category": "Science & Technology", "isShort": false
}
```

Three ready-made views: **Videos**, **Channels** and **Engagement**. Set `flattenOutput: true` for CSV.

***

### Honest limits

- **No transcripts.** YouTube stopped serving caption tracks to anonymous clients; the endpoint returns zero bytes. Anything offering bulk transcripts is running a browser fleet or an authenticated session.
- **No comment text.** Only the count, and only when engagement is enabled.
- **Subscriber counts are rounded.** See above.
- **Region-restricted videos** will not resolve outside their country. Set `country` to reach them.
- **`videosScraped` describes this run**, not the channel. A cap of 10 gives cadence and median over the last 10 uploads, not the channel's lifetime.
- **A private, deleted or age-restricted video** returns `VIDEO_NOT_FOUND` and is **not charged**. Failed rows carry no figures at all — a row that says it could not read the video will never also report a like count.
- **`viewCountIsExact: false`** means `viewCount` came from YouTube's rounded display text. See above.
- **Empty `keywords` and a null `category`** mean the per-video lookup was refused for that row; `publishedAt` is estimated in that case too.

***

### Pricing

| Event | Price | When |
|---|---|---|
| Actor start | $0.005 | Once per run |
| Channel scraped | $0.004 | Per channel profiled |
| Video scraped | $0.0003 | Per video returned, after filters |

**$0.30 per 1,000 videos.** Tracking 50 creators with their 20 most recent uploads costs about **$0.51**. Channels and videos that cannot be resolved are never charged.

***

### Tips

- **Rank by `viewsPerDay`, not `viewCount`.** It is the only fair comparison across videos of different ages.
- **`medianViewsScraped ÷ subscriberCount`** is the number influencer marketers actually want: what share of an audience shows up.
- **`uploadIntervalDays` climbing run over run** means a channel is slowing down — usually before its view counts show it.
- **`publishedWithinDays: 1` with a daily schedule** is a cheap new-upload feed across a whole competitive set.
- **Leave `includeEngagement` off** unless you specifically need likes. It is the single biggest cost lever in this Actor.

# Actor input Schema

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

Handles, URLs or channel ids, one per line: '@mkbhd', 'https://www.youtube.com/@mkbhd', 'UCBJycsmduvYEL83R\_U4JriQ'. Old /c/ and /user/ URLs work too.

## `videos` (type: `array`):

Video URLs or 11-character ids. Accepts watch, youtu.be, /shorts/ and /live/ links. Use this when you want specific videos rather than a whole channel.

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

Alternative to Channels and Videos, for feeding output from another Actor straight into this one.

## `sourceDatasetId` (type: `string`):

Read channel or video identifiers from an existing dataset instead of typing them.

## `sourceDatasetField` (type: `string`):

Which field on the source dataset holds the identifier.

## `maxVideosPerChannel` (type: `integer`):

How many recent uploads to return per channel. YouTube serves 30 per page, so this is your main billing guard. Set 0 to fetch channel statistics only and no videos at all.

## `includeShorts` (type: `boolean`):

Also read the channel's Shorts tab, which YouTube keeps separate from regular uploads. Each returned Short carries isShort: true. A Shorts thumbnail shows no duration and no date, so those come only from "Exact view counts and dates"; without it a Short's durationSeconds and publishedAt are null.

## `publishedWithinDays` (type: `integer`):

Keep only recent uploads. Applied after fetching, because YouTube's channel tab has no date filter. 0 keeps everything.

## `exactVideoStats` (type: `boolean`):

Resolve every video individually to get its exact view count and an exact publish timestamp. Without this you get YouTube's rounded display text — '1.5M views', '2 weeks ago' — which is cheaper but imprecise. Adds one small request per video.

## `includeEngagement` (type: `boolean`):

Fetch each video's like total and comment count. This needs a second, much larger request per video — roughly 25 times the data — so it makes runs slower and more expensive. Comment counts are rounded by YouTube itself.

## `includeDescription` (type: `boolean`):

Store each video's full description. Off by default because descriptions dominate dataset size; a 300-character snippet is always included.

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

Two-letter code used for YouTube's display text, e.g. 'en', 'de', 'es'. Affects the wording of relative dates and view counts, not the numbers.

## `country` (type: `string`):

Two-letter code for the storefront YouTube serves, e.g. 'US', 'GB'. Some videos are region-restricted and will not resolve outside their country.

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

Channels processed in parallel. Responses are small, so this can be raised comfortably.

## `requestTimeoutSecs` (type: `integer`):

Per-request timeout.

## `maxRetries` (type: `integer`):

Retries for connection resets and 5xx responses.

## `flattenOutput` (type: `boolean`):

Emit flat dot-notation columns with arrays joined by ' | '. Use for spreadsheets; leave off for warehouses.

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

Optional. YouTube's own endpoints serve this data to anonymous clients, so a proxy is only worth enabling for very large runs or to read a specific country's catalogue.

## Actor input object example

```json
{
  "channels": [
    "@mkbhd",
    "@MrBeast"
  ],
  "sourceDatasetField": "channelUrl",
  "maxVideosPerChannel": 50,
  "includeShorts": false,
  "publishedWithinDays": 0,
  "exactVideoStats": true,
  "includeEngagement": false,
  "includeDescription": false,
  "language": "en",
  "country": "US",
  "maxConcurrency": 8,
  "requestTimeoutSecs": 45,
  "maxRetries": 2,
  "flattenOutput": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Channel records carry recordType 'channel'; video records carry recordType 'video'.

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

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

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

```

## MCP server setup

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