# YouTube Channel Scraper & Monitor - New Videos, Live Alerts (`lukehunter/youtube-stats-tracker`) Actor

YouTube channel monitor: scrape channels by URL or @handle for subscribers, YouTube new videos, uploads, Shorts and live streams. YouTube alerts for new uploads to Discord, Telegram, Slack or webhook. No login, no proxies, no API key. $0.0013 per row.

- **URL**: https://apify.com/lukehunter/youtube-stats-tracker.md
- **Developed by:** [Luke Hunter](https://apify.com/lukehunter) (community)
- **Categories:** Social media, Automation
- **Stats:** 2 total users, 1 monthly users, 90.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.30 / 1,000 results

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/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 Channel Scraper & Monitor - New Videos, Live Alerts

**A YouTube channel monitor: YouTube new videos, uploads, Shorts and live streams for any list of channels - plus discover similar channels for influencer research.** Give it channel URLs or @handles and it returns one row per channel - subscribers and video count - plus one row per item on the tabs you pick: the **Videos** tab (title, views, age, length), the **Shorts** tab (title, views) and the **Live** tab of past streams (title, views, age, length), each read from the channel's public page. Add the **Featured channels** tab and each creator's channel page also yields the channels they feature - a ready list of related creators with subscriber counts - and `discoverDepth: 1` reads those channels too, so three seed channels become a creator list in one run. Turn on **change mode** (`onlyChangedSinceLastRun`), put it on a schedule, and every run delivers **only what is new since the last run - new videos, new Shorts and new streams, each tracked on its own** - so you can route fresh uploads from the channels you follow, compete with or sponsor straight to Slack, Google Sheets, email or your own pipeline. Built for creators, agencies, influencer-marketing and brand teams, newsrooms, researchers and analysts.

Pay-per-result: **$0.0013 per row - 1,000 rows = $1.30.** No start fee, no login, no proxies, no API key, and in change mode nothing unchanged is charged.

It is deliberately narrow: channel pages only, and what a tab shows on its first page - about **30 newest videos, 48 newest Shorts, a channel's past streams and the handful of channels it features (usually 3 to 13)**. It does not open video pages, search YouTube, or read comments or transcripts (see [Limits](#limitations) for exactly why).

### Quick start (1 minute)

1. Open the **Input** tab and paste YouTube channel URLs (`https://www.youtube.com/@mkbhd`) or bare @handles.
2. Choose the **Tabs to read**: Videos (the default), Shorts, Live streams, Featured channels - any combination.
3. Click **Start**, then export the dataset to CSV/Excel/JSON or pull it through the API shown below.

```json
{
  "channelUrls": [
    "https://www.youtube.com/@mkbhd",
    "@veritasium"
  ],
  "tabs": ["videos", "shorts", "streams"],
  "output": "channelsAndVideos",
  "maxItems": 300
}
```

Leave `tabs` out and you get the Videos tab only, exactly as before this option existed. Every common channel link form works: `/@handle`, `/channel/UC...`, `/c/<name>`, `/user/<name>`, a bare `@handle`, and a link with a tab such as `/videos` or `/shorts` (the link's own tab is ignored - the `tabs` input decides). **Video links are not accepted** (watch, youtu.be, shorts): the run stops at once with a clear message, because YouTube asks automated clients to sign in on video pages and this Actor never bypasses that. Paste the channel URL instead.

**What one channel costs per tab.** Every channel gives one channel row, once, however many tabs you pick; each tab adds a row per item it shows. At $0.0013 per row:

| Tabs read for one channel | Rows | Cost |
|---|---:|---:|
| Videos (the default): channel row + up to 30 videos | about 31 | about $0.0403 |
| Shorts: channel row + up to 48 Shorts | about 49 | about $0.0637 |
| Live: channel row + its past streams (@mkbhd shows 5, @TED shows 11) | about 6-12 | about $0.0078-$0.0156 |
| Videos + Shorts + Live: one channel row + all three tabs (@mkbhd: 30 + 48 + 5) | about 84 | about $0.1092 |
| Featured channels: channel row + the channels it features (@mkbhd 6, @veritasium 12, @TED 3) | about 4-13 | about $0.0052-$0.0169 |
| `output: "channelsOnly"`: just the channel row | 1 | $0.0013 |

The default `maxItems` of 100 covers about three channels on Videos alone. Raise `maxItems` for longer lists or more tabs - it is your cost cap.

### YouTube channel monitor: what you can use it for

- **New-video alerts for YouTube channels.** Follow competitors, partners, news sources or creators you sponsor: schedule the Actor in change mode and get a row for each new upload, with its title, link and length, as soon as a run sees it.
- **YouTube channel monitoring.** Track `subscriberCountText` and `videoCountText` for a watch list of channels over time; in change mode a channel row arrives when either displayed figure moves.
- **Creator and competitor research.** Put 50 channels in one run and get each channel's subscribers, video count and latest uploads with rounded views, ages and lengths, ready for a spreadsheet.
- **Discover similar channels for influencer research.** Add the Featured channels tab to see which creators a channel features, with their subscriber counts; set `discoverDepth` to 1 and those channels are read too, giving you a creator list to qualify for sponsorships, collaborations or outreach lists. See the recipe below.

### Example output

Videos, Shorts, streams and featured channels each have their own row `type`: `video`, `short`, `stream` and `featuredChannel`. The first two video rows from a real page (youtube.com/@mkbhd/videos):

```json
{
  "type": "video",
  "channelId": "UCBJycsmduvYEL83R_U4JriQ",
  "channelName": "Marques Brownlee",
  "channelHandle": "@mkbhd",
  "videoId": "3iRUwVzRDZQ",
  "url": "https://www.youtube.com/watch?v=3iRUwVzRDZQ",
  "title": "Xiaomi 18 Fold: How Does This Happen?",
  "viewCountText": "6.1 million views",
  "viewCount": 6100000,
  "publishedText": "1 day ago",
  "durationText": "10:47",
  "durationSeconds": 647,
  "position": 1,
  "isNew": null,
  "scrapedAt": "2026-10-04T08:00:00.000Z"
}
```

```json
{
  "type": "video",
  "channelId": "UCBJycsmduvYEL83R_U4JriQ",
  "channelName": "Marques Brownlee",
  "channelHandle": "@mkbhd",
  "videoId": "e1q-TuHdc4Y",
  "url": "https://www.youtube.com/watch?v=e1q-TuHdc4Y",
  "title": "Dear YouTube!",
  "viewCountText": "5.2 million views",
  "viewCount": 5200000,
  "publishedText": "3 days ago",
  "durationText": "12:23",
  "durationSeconds": 743,
  "position": 2,
  "isNew": null,
  "scrapedAt": "2026-10-04T08:00:00.000Z"
}
```

`position` 1 is the newest video on the channel. `isNew` is `null` in a normal run and on the first run of change mode (nothing is known yet) and `true` for a video delivered because it was not seen before.

A **Short** row (`tabs` includes `shorts`; first row of youtube.com/@mkbhd/shorts). The Shorts tab shows a title and a view count and nothing else, so `publishedText`, `durationText` and `durationSeconds` are always `null` - never guessed - and the link is a `/shorts/` address for you to open:

```json
{
  "type": "short",
  "channelId": "UCBJycsmduvYEL83R_U4JriQ",
  "channelName": "Marques Brownlee",
  "channelHandle": "@mkbhd",
  "videoId": "R6yNUnRXZ64",
  "url": "https://www.youtube.com/shorts/R6yNUnRXZ64",
  "title": "iPhone Duo is here",
  "viewCountText": "7.4M views",
  "viewCount": 7400000,
  "publishedText": null,
  "durationText": null,
  "durationSeconds": null,
  "position": 1,
  "isNew": null,
  "scrapedAt": "2026-10-04T08:00:00.000Z"
}
```

A **stream** row (`tabs` includes `streams`; first row of youtube.com/@mkbhd/streams): the video fields plus `liveStatus`, which is `"past"` for a finished stream (the Live tab words it "Streamed 8 years ago") and `null` whenever the page does not say so:

```json
{
  "type": "stream",
  "channelId": "UCBJycsmduvYEL83R_U4JriQ",
  "channelName": "Marques Brownlee",
  "channelHandle": "@mkbhd",
  "videoId": "49-rK7SAfQk",
  "url": "https://www.youtube.com/watch?v=49-rK7SAfQk",
  "title": "5 New Phone Updates + Giveaway Update!",
  "viewCountText": "944 thousand views",
  "viewCount": 944000,
  "publishedText": "Streamed 8 years ago",
  "durationText": "41:41",
  "durationSeconds": 2501,
  "liveStatus": "past",
  "position": 1,
  "isNew": null,
  "scrapedAt": "2026-10-04T08:00:00.000Z"
}
```

A **featuredChannel** row (`tabs` includes `channels`; the first of the 6 channels @mkbhd features, from a real page). It says which channel features it (`sourceChannelId`, `sourceHandle`); `channelId` and `url` are the featured channel's own, ready to feed back into `channelUrls`:

```json
{
  "type": "featuredChannel",
  "sourceChannelId": "UCBJycsmduvYEL83R_U4JriQ",
  "sourceHandle": "@mkbhd",
  "channelId": "UCG7J20LhUeLl6y_Emi7OJrA",
  "handle": "@TheStudio",
  "name": "The Studio",
  "url": "https://www.youtube.com/channel/UCG7J20LhUeLl6y_Emi7OJrA",
  "subscriberCountText": "1.16M subscribers",
  "subscriberCount": 1160000,
  "videoCountText": "219 videos",
  "position": 1,
  "isNew": null,
  "scrapedAt": "2026-10-04T08:00:00.000Z"
}
```

The channel row from the same pages (description shortened here for space):

```json
{
  "type": "channel",
  "channelId": "UCBJycsmduvYEL83R_U4JriQ",
  "url": "https://www.youtube.com/channel/UCBJycsmduvYEL83R_U4JriQ",
  "handle": "@mkbhd",
  "name": "Marques Brownlee",
  "subscriberCountText": "21.3M subscribers",
  "subscriberCount": 21300000,
  "videoCountText": "1.8K videos",
  "videoCount": 1800,
  "description": "MKBHD: Quality Tech Videos | YouTuber | Geek | Consumer Electronics | Tech Head | Internet Personality! ...",
  "avatarUrl": "https://yt3.googleusercontent.com/qu4TmIaYUlS41-dJ9gZ7DUR3nilvmB5_11i6OKSdvNnBNiyOusZP1bMN6ICnuxtjFBb6ioKgRQ=s900-c-k-c0x00ffffff-no-rj",
  "status": "ok",
  "statusReason": null,
  "changedFields": null,
  "newVideoIds": null,
  "scrapedAt": "2026-10-04T08:00:00.000Z"
}
```

**Numbers are shown the way YouTube shows them: rounded.** A channel page says "21.3M subscribers", "1.8K videos" and "6.1 million views", so `subscriberCount`, `videoCount` and `viewCount` are whole numbers parsed from that text and are **approximate**. The text exactly as YouTube shows it is always kept next to them (`subscriberCountText`, `videoCountText`, `viewCountText`). `publishedText` is a relative age ("3 days ago"), not a date.

A channel that answers 404 is delivered once as a single `unavailable` channel row with everything the page would have given set to `null` - no tab rows, whichever tabs you asked for.

### Recipe: build a creator list for influencer research

Seed three channels in your niche and let the Actor add the channels they feature. One run, no search, no login:

```json
{
  "channelUrls": [
    "https://www.youtube.com/@mkbhd",
    "@veritasium",
    "https://www.youtube.com/@LinusTechTips"
  ],
  "tabs": ["channels"],
  "discoverDepth": 1,
  "maxDiscoveredChannels": 20,
  "maxItems": 100
}
```

What you get, in this order:

1. For each seed: its **channel row** (subscribers, video count, description, avatar) and one **featuredChannel row** per channel it features - on the real pages @mkbhd features 6, @veritasium 12 and @LinusTechTips 12.
2. Then one **channel row** for each channel found (de-duplicated by `channelId`, up to `maxDiscoveredChannels`, 20 by default), marked `discoveredFrom` with the channelId of the seed that featured it. Filter on `subscriberCount` and `videoCount` to qualify them, and sort by `discoveredFrom` to see which seed each came from.

Add `"videos"` to `tabs` and each discovered channel also delivers its latest videos (the same tabs as the seeds, except `channels`): a creator list with recent output in one dataset. Discovery goes **one hop only**: the channels a discovered channel features are never read, so the run cannot snowball. `discoverDepth: 1` needs `"channels"` in `tabs`; `maxItems` still caps every row, and pacing and `robots.txt` rules are the same as for any channel.

**Cost example** (at $0.0013 per row): the input above is 3 seed channel rows + 30 featured-channel rows + 20 discovered channel rows = **53 rows, about $0.0689**. Every one of those rows is a normal delivered result; set `maxDiscoveredChannels` lower, or `maxItems`, to spend less. With `"tabs": ["videos", "channels"]` each discovered channel adds up to 31 rows (its channel row and 30 videos, about $0.04), so keep `maxDiscoveredChannels` small or `maxItems` tight there.

Run it again later with `onlyChangedSinceLastRun: true`: only newly featured channels (`isNew: true`, with the source that now features them) and channels whose displayed counts changed are delivered.

### Alert on new uploads: schedule + Slack, Sheets or webhook

Change mode makes this a new-upload feed. **The first run delivers everything** (the channel row with `changedFields: ["firstSeen"]` and the items of every tab you selected, `isNew: null`) **and remembers it.** Every run after that delivers:

- a **video, short, stream or featuredChannel row** (`isNew: true`) for each item not seen before on that channel **and tab**, newest first (a featuredChannel row means a channel newly featured by that source channel; each source channel has its own list) - nothing else about an item can trigger a row, so rounded view-count growth costs nothing;
- a **channel row** only when its displayed subscriber count, video count or status changed (`changedFields` says which; `newVideoIds` lists any new uploads, Shorts and streams seen in the same run).

**Each tab is tracked on its own.** Add `"shorts"`, `"streams"` or `"channels"` to a watch that already exists and that tab gets a quiet baseline: its items are delivered once, on that run, as the first run for that tab (`isNew: null`), and remembered - the following runs report only genuinely new Shorts, streams or featured channels, never the 48 you just saw as "new". State written before the `tabs` option existed keeps working unchanged.

1. Save this as a Task (your watch list, with change mode on):

```json
{
  "channelUrls": [
    "https://www.youtube.com/@mkbhd",
    "@veritasium",
    "https://www.youtube.com/@LinusTechTips"
  ],
  "tabs": ["videos", "shorts"],
  "output": "channelsAndVideos",
  "onlyChangedSinceLastRun": true,
  "maxItems": 500
}
```

2. Add a **Schedule** (hourly to catch uploads quickly, daily for a digest).
3. In the Task's **Integrations** tab add one or more of:
   - **Slack** (or email) on "Run succeeded", sending the dataset - every run that finds new uploads posts them; a run with nothing new has an empty dataset, so there is nothing to post;
   - **Google Sheets**: append each run's dataset rows to a sheet, a running log of every new upload;
   - a **Webhook** for `ACTOR.RUN.SUCCEEDED` (or Make, Zapier, n8n) that fetches the run's dataset and acts on rows where `type` is `video`, `short` or `stream` and `isNew` is `true`.
4. Add channels to the Task whenever you like. A channel added later gets its own baseline run (everything delivered once); the others stay quiet until they post.

A run where nothing is new **succeeds** with an empty dataset and charges nothing; the run's status message says so, e.g. "Checked 3 of 3 URL(s). 3 unchanged. Nothing to deliver or charge."

Steady-state cost is tiny: $0.0013 per new upload, Short or stream. A channel row only appears when YouTube's rounded subscriber or video figure changes (for example 21.3M to 21.4M), which for a large channel is rare. With `output: "channelsOnly"` change mode returns just the channel row, and also delivers it when a new upload appears (`changedFields` includes `newVideos`, `newVideoIds` lists the videos), so it still works as an upload alert at one row per event.

State is kept in a named key-value store (`stateStoreName`, default `youtube-stats-tracker-state`), so it survives between runs. Use another name to keep separate histories for separate tasks, or delete the store to start over with a fresh baseline.

### Get new YouTube video alerts in Discord, Telegram, Slack, Teams, Google Chat, your phone (ntfy/Pushover) or any app (webhook)

Want a **new YouTube video alert** in Discord, Telegram, Slack or on your phone instead of reading rows from a dataset? Turn on change mode (`onlyChangedSinceLastRun`), add one or more destinations in the Actor's **Notifications (optional)** section, and each run sends the new uploads it delivered, with the title, link, views, length and thumbnail. It is a **monitor that notifies you**: it only reads public channel pages, and it never subscribes, comments, likes or logs in to YouTube.

| Channel | Fill in | What you get |
|---|---|---|
| Discord | `discordWebhookUrl` | One card per update, 10 per message |
| Telegram | `telegramBotToken` + `telegramChatId` | Up to 10 updates per message, one line each |
| Slack | `slackWebhookUrl` | Block Kit message, up to 10 updates per message |
| Mattermost, Rocket.Chat | `slackCompatibleWebhookUrl` | Slack-style incoming webhook: a Markdown list (these apps do not render Slack Block Kit) |
| Microsoft Teams | `teamsWebhookUrl` | Adaptive Card from a Teams Workflows webhook, up to 10 updates per card |
| Google Chat | `googleChatWebhookUrl` | Card in a space, up to 10 updates per message |
| ntfy (phone push, no account) | `ntfyTopic` (optional `ntfyServer`, `ntfyToken`) | A push per update, up to 10, then one "...and N more" |
| Pushover (phone push) | `pushoverUserKey` + `pushoverAppToken` | A push per update, up to 10, then one "...and N more" |
| Any app: Zapier, Make, n8n, Pipedream, IFTTT, Home Assistant | `webhookUrl` | One JSON POST per run with every delivered row |

Fill in as many as you like; each channel works on its own and a failing one never stops the others.

#### Setup, channel by channel

- **Discord (1 minute):** Server Settings → Integrations → Webhooks → New Webhook → pick the channel → **Copy Webhook URL**.
- **Telegram (2 minutes):** chat with **@BotFather**, send `/newbot`, paste the token into `telegramBotToken`. Add the bot to the chat or channel (administrator for a channel; for a chat with yourself press **Start** on the bot). `telegramChatId` is a numeric id (`123456789`, or `-1001234567890` for a group or channel) or a public `@channelusername`.
- **Slack:** at api.slack.com/apps create an app (**From scratch**) → **Incoming Webhooks** → switch it on → **Add New Webhook to Workspace** → pick the channel → copy the `https://hooks.slack.com/services/...` URL.
- **Mattermost or Rocket.Chat:** create an **incoming webhook** in the app and paste its public `https://` URL into `slackCompatibleWebhookUrl`. It must be reachable from the internet: localhost, private IP addresses and `.local` / `.internal` names are rejected.
- **Microsoft Teams:** in the channel open **... → Workflows** → **Post to a channel when a webhook request is received** → finish the wizard and copy the URL (a host ending in `.logic.azure.com` or `.powerplatform.com`). The old "Office 365 connector" webhooks are retired and not supported.
- **Google Chat:** in the space open **Apps & integrations → Webhooks → Add webhook**, name it and copy the `https://chat.googleapis.com/v1/spaces/...` URL (needs a Google Workspace account that allows webhooks).
- **ntfy (free, no account):** install the ntfy app (or open ntfy.sh), subscribe to a topic with a long random name, and put the same name in `ntfyTopic`. Anyone who knows the topic can read it, so make it unguessable. Own server or a protected topic: set `ntfyServer` (public `https://` address) and `ntfyToken`.
- **Pushover:** copy your user key from pushover.net into `pushoverUserKey`, create an application at pushover.net/apps/build and put its API token in `pushoverAppToken`. Pushover itself is a separate paid app after its trial: check their pricing.
- **Any app (webhook):** paste the catch-hook URL from Zapier, Make, n8n, Pipedream, IFTTT or Home Assistant into `webhookUrl`. It must be a public `https://` address.

#### The recipe: a new-upload alert for the channels you follow, in five steps

1. Put the channels in `channelUrls`, and add `shorts` and `streams` to `tabs` if you want those too.
2. Turn on `onlyChangedSinceLastRun`.
3. Add your destination(s) from the table above.
4. Run it once. The first run is the baseline: it records what is already there and sends nothing, so you are not flooded with old videos.
5. Create a **Schedule** (hourly to catch uploads quickly, daily for a digest). From then on each run sends only what is new.

```json
{
  "channelUrls": [
    "https://www.youtube.com/@mkbhd",
    "@veritasium"
  ],
  "tabs": [
    "videos",
    "shorts"
  ],
  "output": "channelsAndVideos",
  "onlyChangedSinceLastRun": true,
  "maxItems": 500,
  "discordWebhookUrl": "https://discord.com/api/webhooks/<id>/<token>",
  "ntfyTopic": "your-long-random-topic-name",
  "maxNotifications": 25
}
```

#### The generic webhook (`webhookUrl`)

Each run sends **one POST per 100 delivered rows** (so usually one) with this body, followed by whatever your tool does with it:

```json
{
  "source": "apify",
  "actor": "youtube-stats-tracker",
  "runId": "HG7ML7M8z78YcAPEB",
  "count": 2,
  "total": 2,
  "part": 1,
  "parts": 1,
  "items": [
    {
      "...": "the full dataset rows"
    }
  ]
}
```

`count` is the number of rows in this POST, `total` the number delivered in the run, and `part` / `parts` number the POSTs when a run delivers more than 100 rows. `maxNotifications` does not cap the webhook: your tool gets every delivered row. Your endpoint should answer with a 2xx status; redirects are not followed and a failure is reported in the run status, never retried in a loop.

#### What each message contains

In change mode these delivered rows are announced, uploads first:

- **A new video, Short or stream** (`isNew: true`): `New video: <title> — <channel>`, `New Short: ...` or `New stream: ...`, with the link, views, age and length that YouTube shows (a Short shows neither age nor length, so they are left out), the channel's @handle, and the video's thumbnail. The thumbnail is YouTube's standard public still for the video id; the dataset has no thumbnail field and this Actor never requests the image, only the chat service showing it does.
- **A channel change** (a channel row whose displayed figures moved): `Subscribers: 1.2M → 1.3M — <channel>`, `Videos: 1.9K → 2K — <channel>`, `Channel unavailable` or `Channel is back`, with the channel's picture. These figures are YouTube's rounded displays, so a small channel changes often and a large one rarely. With `output: "channelsOnly"` the channel row is also the upload signal, so a new upload is announced as `New upload: <channel> (1 new)` linking to the channel (use `channelsAndVideos` to get a link to each video).
- **A newly featured channel** (`featuredChannel`, `isNew: true`, only when `tabs` includes `channels`): `New featured channel: <name> — featured by <source>`, with its subscribers and video count.

Never announced: the baseline run (the first run for a watch, or for a newly added channel or tab: those rows have `isNew: null` or `firstSeen`), rounded view-count growth (not a change at all), and any run that is not in change mode. Discord gets a card per item, Telegram one line per item, Slack Block Kit sections, Teams an Adaptive Card, Google Chat a card, and ntfy/Pushover a push that opens the video or channel. Link previews are off, and titles are escaped so a title can never inject formatting or mentions.

#### What to expect

- **Only delivered rows are sent, and only in change mode.** A normal run delivers every row rather than only what is new, so it sends nothing (the status message says so). A run with nothing new sends nothing, so there is no "no new videos" spam. Rows the run could not deliver (for example because your spending limit was reached) are not sent.
- **`maxNotifications` (default 25, max 100)** caps how many updates go to chat and phone channels per run (the webhook is not capped). The rest are still in the dataset and the status message says so. ntfy and Pushover send at most 10 individual pushes, then one "...and N more" summary.
- **Cost: notifications are free.** You pay only the usual price for delivered rows, exactly as without alerts. There is no extra charge for any channel.
- **Failures never fail the run.** A deleted webhook or an unreachable service leaves the run successful and the dataset complete, and the status message says what happened, for example `Notified: Discord 12, Slack 12, ntfy 10 (+summary). Teams failed (HTTP 404) - check the webhook URL.` The logs show the channel name and HTTP status only, never a URL or token.
- **Your URLs, tokens and ntfy topic are stored as secret inputs.** Anyone holding a webhook URL can post to that channel, so treat it like a password.
- **It is a monitor that notifies you.** It never subscribes, comments, likes or logs in to YouTube. The messages go only to the destinations you set up.
- **Nothing changes by default.** With no destination set, a run behaves exactly as before and the dataset rows are identical.

### How it works

For each channel URL or @handle the Actor:

1. Reduces it to the channel and requests one page per tab you selected: `https://www.youtube.com/<channel>/videos`, `/shorts`, `/streams` (the Live tab) and/or `/channels` (YouTube answers that address with the channel's Home page, where the channels it features sit). Each page carries the channel header and that tab's newest items, so the channel row is built once, from the first page read (tracking parameters and tabs in your link are dropped; the same channel listed twice is read once).
2. Reads `robots.txt` once per run and checks every path - and every redirect - against it before requesting it.
3. Reads the channel and the tab's items from the data embedded in the page's HTML, the same data YouTube's own page is drawn from.
4. With `discoverDepth: 1`, adds the featured channels it found to the run (after the channels you listed, de-duplicated, capped) and reads each with the same tabs except `channels`.
5. Delivers flat rows, charging only for rows it actually delivers.

It reads politely - about one request every 2 seconds, with an honest, self-identifying User-Agent - and it never signs in. If YouTube answers with a bot check or a consent page it reports that and stops, instead of working around it.

### Input

| Field | Type | Default | Description |
|---|---|---:|---|
| `channelUrls` | string\[] | *(required)* | YouTube channel URLs or @handles. Up to 5,000 per run. Video links are rejected. |
| `tabs` | string\[] | `["videos"]` | Which tabs to read: any of `videos`, `shorts`, `streams` (the Live tab), `channels` (featured channels). Duplicates collapse; tabs are read in that fixed order. |
| `discoverDepth` | integer | `0` | `0`: only the channels you list. `1`: every featured channel found becomes an extra channel of the run (needs `channels` in `tabs`), read with the same tabs except `channels` - one hop, never deeper. |
| `maxDiscoveredChannels` | integer | `20` | 1-200. Cap on the extra channels discovery adds. Ignored when `discoverDepth` is 0. |
| `output` | string | `channelsAndVideos` | `channelsAndVideos`: a channel row plus a row per item on each selected tab. `channelsOnly`: just the channel row. |
| `outputFormat` | string | `full` | `full`: every field (default). `compact`: a smaller row for AI agents (see "Use with AI agents (MCP)"). Charging is identical. |
| `maxItems` | integer | 100 | 1-5,000. Hard cap on rows delivered in the run (channel, video, short, stream and featuredChannel rows together) - your cost cap. |
| `onlyChangedSinceLastRun` | boolean | `false` | Change mode: only new items per tab, plus a channel row when its displayed counts changed. |
| `stateStoreName` | string | `youtube-stats-tracker-state` | Name of the store that remembers what was seen in change mode. Letters, numbers and hyphens. |
| `discordWebhookUrl` | string (secret) | *(none)* | Discord webhook URL. Must look like `https://discord.com/api/webhooks/<id>/<token>`. Empty means no Discord messages. See the alerts section above. |
| `telegramBotToken` | string (secret) | *(none)* | Telegram bot token from @BotFather. Needs `telegramChatId`. |
| `telegramChatId` | string | *(none)* | Telegram chat to post to: a numeric chat id or `@channelusername`. The bot must be added to that chat or channel. Needs `telegramBotToken`. |
| `slackWebhookUrl` | string (secret) | *(none)* | Slack incoming webhook, `https://hooks.slack.com/services/...`. |
| `slackCompatibleWebhookUrl` | string (secret) | *(none)* | Mattermost / Rocket.Chat incoming webhook (public `https://` URL; Markdown list, no Block Kit). |
| `teamsWebhookUrl` | string (secret) | *(none)* | Microsoft Teams Workflows webhook (host ending `.logic.azure.com` or `.powerplatform.com`). |
| `googleChatWebhookUrl` | string (secret) | *(none)* | Google Chat space webhook, `https://chat.googleapis.com/v1/spaces/...`. |
| `ntfyTopic` | string (secret) | *(none)* | ntfy topic name; setting it turns ntfy phone pushes on. Pick a long, unguessable name. |
| `ntfyServer` | string | `https://ntfy.sh` | ntfy server. Only change it for your own public `https://` ntfy server. |
| `ntfyToken` | string (secret) | *(none)* | Optional ntfy access token for a self-hosted server or protected topic. Needs `ntfyTopic`. |
| `pushoverUserKey` | string (secret) | *(none)* | Pushover user key. Needs `pushoverAppToken`. |
| `pushoverAppToken` | string (secret) | *(none)* | Pushover application API token. Needs `pushoverUserKey`. |
| `webhookUrl` | string (secret) | *(none)* | Generic JSON webhook (Zapier, Make, n8n, Pipedream, IFTTT, Home Assistant): every delivered row, 100 per POST. Public `https://` only. |
| `maxNotifications` | integer | 25 | 1–100. Most updates sent to chat and phone channels per run (the webhook is not capped). Extra updates are still in the dataset. Only used when a destination is set. |

#### Input details

The Store form and the MCP tool schema show a short description for each field (an AI agent sees only the first 500 characters of each); the longer rules are here.

- **`channelUrls`**: accepted forms are `https://www.youtube.com/@mkbhd`, `/channel/UC...`, `/c/<name>`, `/user/<name>` or just the `@handle`. A tab on the end (`/videos`, `/shorts`, `/featured`) is ignored (choose the tabs in `tabs`), tracking parameters are cleaned up, and the same channel listed twice is read once. Up to 5,000 per run. Video links (`watch`, `youtu.be`, `shorts`) are NOT supported and stop the run with a clear message: YouTube asks automated clients to sign in on video pages and this Actor never bypasses that. Pass the channel URL instead.
- **`tabs`**: `videos` is the Videos tab (about 30 latest), `shorts` the Shorts tab (about 48 latest), `streams` the Live tab (past streams) and `channels` the channels a creator features (usually 3 to 13, one `featuredChannel` row each, for influencer discovery). Each tab is one page request per channel and one billed row per item shown, plus a single channel row per channel however many tabs you pick. In change mode every tab is tracked on its own, so "new uploads only" works for Shorts and streams too, and a newly featured channel comes with `isNew` true; a tab you add later is delivered once as its first run and then only its new items.
- **`discoverDepth`**: with `1`, every featured channel found on the channels you list (needs `channels` in `tabs`) is also read as an extra channel of this run, with the same tabs except `channels`: one hop, never deeper. Discovered channels are de-duplicated by channel ID, capped by `maxDiscoveredChannels`, marked `discoveredFrom` (the channel that featured them) on their channel row, and count against `maxItems` like any other row. Same pacing and `robots.txt` rules.
- **`maxDiscoveredChannels`**: each extra channel costs one request and at least one billed channel row, so together with `maxItems` this is your cost cap. Ignored when `discoverDepth` is 0.
- **`output`**: `channelsAndVideos` returns one channel row plus one row per item on each selected tab (about 30 videos, 48 shorts or a channel's past streams, newest first). `channelsOnly` returns just the channel row. Every delivered row is one billed result, so a channel with 30 videos is up to 31 rows.
- **`maxItems`**: you are charged $0.0013 per delivered row. One channel can be up to 31 rows with the Videos tab, 49 with Shorts, and about 84 with Videos, Shorts and Live; the `channels` tab adds one row per featured channel (usually 3 to 13), and each discovered channel at least one more.
- **`onlyChangedSinceLastRun`**: delivers a row for each video, short or stream not seen before on that channel and tab (for the tabs you selected), plus a channel row when its displayed subscriber count or video count changed. Nothing unchanged is delivered or charged. The first run delivers everything and remembers it.
- **`stateStoreName`**: only used in change mode; remembers which videos, shorts and streams and which subscriber and video counts were already seen for each channel, across scheduled runs. Leave the default unless several tasks must keep separate histories.

### Output fields

**Channel rows** (`type: "channel"`)

| Category | Fields |
|---|---|
| Identity | `channelId`, `url`, `handle`, `name`, `avatarUrl`, `description` (first 5,000 characters) |
| Numbers | `subscriberCountText` + `subscriberCount` (approximate), `videoCountText` + `videoCount` (approximate) |
| Status | `status` (`ok` / `unavailable`), `statusReason`, `changedFields` (change mode), `newVideoIds` (change mode), `scrapedAt` |
| Discovery | `discoveredFrom` - only on channels found by `discoverDepth: 1`: the channelId of the channel that featured it |

**Video rows** (`type: "video"`, the Videos tab)

| Category | Fields |
|---|---|
| Identity | `channelId`, `channelName`, `channelHandle`, `videoId`, `url`, `title` |
| Numbers | `viewCountText` + `viewCount` (approximate), `publishedText`, `durationText` + `durationSeconds` |
| Position | `position` (1 = newest), `isNew` (change mode), `scrapedAt` |

**Short rows** (`type: "short"`, the Shorts tab): the same fields; `url` is `https://www.youtube.com/shorts/<videoId>`, and `publishedText`, `durationText` and `durationSeconds` are always `null` because a Shorts tile shows no date or length.

**Stream rows** (`type: "stream"`, the Live tab): the video fields plus `liveStatus` (`"past"` for a finished stream, `null` when the page does not say). `publishedText` reads like "Streamed 8 years ago".

**Featured channel rows** (`type: "featuredChannel"`, the Channels tab)

| Category | Fields |
|---|---|
| Source | `sourceChannelId`, `sourceHandle` - the channel whose page features this one |
| Identity | `channelId`, `handle`, `name`, `url` (`https://www.youtube.com/channel/<channelId>`) |
| Numbers | `subscriberCountText` + `subscriberCount` (approximate), `videoCountText` (`null` when the page shows none, as for some small channels) |
| Position | `position` (1 = first on the page), `isNew` (change mode), `scrapedAt` |

In the Apify dataset view the four item types have their own tables: Videos, Shorts, Live streams and Featured channels. Missing values are `null`. Nothing is invented: `subscriberCount` is `null` when the channel hides its subscribers, a number that cannot be read from YouTube's text is `null` rather than a guess, `durationSeconds` is `null` for a live stream, and `viewCount` is `null` where a tab shows no views (a few old streams).

### Use it as an API

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")

run = client.actor("lukehunter/youtube-stats-tracker").call(
    run_input={
        "channelUrls": ["https://www.youtube.com/@mkbhd", "@veritasium"],
        "tabs": ["videos", "shorts", "streams"],
        "onlyChangedSinceLastRun": True,
    }
)

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item["type"] in ("video", "short", "stream") and item["isNew"]:
        print("New upload:", item["channelName"], item["title"], item["url"])
    elif item["type"] == "channel" and item["changedFields"] != ["firstSeen"]:
        print("Channel changed:", item["name"], item["subscriberCountText"], item["changedFields"])
```

### Use with AI agents (MCP)

This Actor works as a tool for Claude, ChatGPT, Cursor and other MCP clients through [Apify's MCP server](https://mcp.apify.com). The only input a call needs is `channelUrls`.

**Claude Desktop** (`claude_desktop_config.json`), with your own token from Apify Console > Settings > API & Integrations:

```json
{
  "mcpServers": {
    "apify-youtube": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote",
        "https://mcp.apify.com?tools=lukehunter/youtube-stats-tracker",
        "--header", "Authorization: Bearer ${APIFY_TOKEN}"
      ],
      "env": { "APIFY_TOKEN": "<YOUR_APIFY_TOKEN>" }
    }
  }
}
```

Clients that support OAuth (Claude.ai, VS Code) can connect with just the URL `https://mcp.apify.com?tools=lukehunter/youtube-stats-tracker` and sign in to Apify. Never paste a real token into a shared file or chat.

**Example.** You ask: "How many subscribers does @mkbhd have, and what are his latest videos and their views?" The agent calls the tool `lukehunter--youtube-stats-tracker` with:

```json
{
  "channelUrls": ["@mkbhd"],
  "outputFormat": "compact",
  "maxItems": 10
}
```

`outputFormat: "compact"` returns numeric counts (subscribers, views, duration in seconds), titles, URLs and ids without display strings, so the result fits an agent's context. It is charged exactly like the default `"full"` output: $0.0013 per row, capped by `maxItems`. For a stats check only, add `"output": "channelsOnly"` to get just the channel row.

### Pricing and cost control

Pay per delivered row (a channel, a video, a Short, a stream or a featured channel).

**$0.0013 per row - 1,000 rows = $1.30.**

| What you run | Rows | Cost |
|---|---:|---:|
| 1 channel, Videos tab (channel row + up to 30 videos) | about 31 | about $0.0403 |
| 1 channel, Shorts tab (channel row + up to 48 Shorts) | about 49 | about $0.0637 |
| 1 channel, Live tab (channel row + its past streams) | about 6-12 | about $0.0078-$0.0156 |
| 1 channel, Videos + Shorts + Live (one channel row, @mkbhd: 30 + 48 + 5) | about 84 | about $0.1092 |
| 1 channel, Featured channels tab (channel row + 3-13 featured channels) | about 4-14 | about $0.0052-$0.0182 |
| Creator list: 3 seed channels, `tabs: ["channels"]`, `discoverDepth: 1`, default cap of 20 (3 + 30 featured + 20 discovered) | 53 | about $0.0689 |
| 100 channels, channels only | 100 | $0.13 |
| 100 channels, Videos tab (first run) | about 3,100 | about $4.03 |
| The same 100 channels on a schedule, change mode, per new video, Short or stream | 1 | $0.0013 |
| Cap: 5,000 rows | 5,000 | $6.50 |

Discovery adds nothing to the price per row: every discovered channel is read and charged like a channel you listed yourself, and `maxDiscoveredChannels` and `maxItems` bound the cost. There is no charge for starting a run, and none for a URL that is blocked, skipped by `robots.txt`, invalid, or answered with a bot check or consent page. A channel YouTube answers 404 for is a real answer and is charged as one row. In change mode, unchanged channels and items are not charged. `maxItems` is a hard upper bound on cost, and the Actor never delivers a row your run's maximum-cost setting cannot pay for. If `maxItems` cuts a channel short, the channel row and the first tab's newest items are the ones kept.

Notifications to Discord, Telegram, Slack, Teams, Google Chat, ntfy, Pushover and webhooks are free: you pay only for delivered rows, never for messages sent.

### Limitations

- **Channel tab pages only, and what each tab shows first.** The Actor reads one page per selected tab, `<channel>/videos`, `/shorts`, `/streams` and/or `/channels`, and returns the items YouTube lists on it (about 30 videos, 48 Shorts, a channel's past streams - newest first). It does not page back through a channel's whole catalogue, because that needs YouTube's "load more" API (`/youtubei/`), which `robots.txt` disallows. To follow a channel's output over time, run it on a schedule in change mode: each new upload, Short or stream is delivered as it appears.
- **No video pages, search, comments or transcripts.** Video pages are not read because YouTube asks automated clients to sign in there ("Sign in to confirm you're not a bot"), and this Actor never bypasses that. Search (`/results`), comments (`/comment`) and the paging API are disallowed by YouTube's `robots.txt`. So there are no exact view counts, like counts, descriptions, tags or publish dates for individual videos: you get what the channel's tab pages show.
- **Discovery follows what creators feature, not YouTube's recommendations.** The Featured channels tab lists the channels a creator chooses to show on their own page (their other channels, collaborators, friends - 3 to 13 on the pages we have read). Many channels feature none, so they add no rows; there is no "similar to" ranking, no search for channels by topic (`/results` is disallowed by `robots.txt`) and no paging for more. YouTube answers the `/channels` address with the channel's Home page, and that page's featured-channel shelf is what is read. Discovery goes one hop (`discoverDepth` 1): discovered channels are read, never expanded further. Discovered channels are added in the order found (your channels first, in input order, then each page's order) up to `maxDiscoveredChannels`.
- **Numbers are as YouTube displays them, rounded.** Subscribers ("21.3M subscribers"), the channel's video count and each video's views ("6.1 million views") are rounded by YouTube, so the integer fields are approximate, and a subscriber change is only seen when the displayed figure moves. A video's age is relative ("3 days ago"), not a timestamp.
- **robots.txt is respected, for every path and every redirect.** A disallowed path is skipped and reported, and a `robots.txt` that cannot be read means nothing is read without confirmed permission.
- **YouTube can answer with a bot check or a consent page.** From some networks YouTube sends a bot check or a consent page instead of the channel. The Actor reports it (`blocked` or `consent-required`), delivers no row for it, charges nothing, and stops sending requests for the rest of the run. It does not sign in, use cookies, proxies or rotating identities, click through consent, or solve anything. Rows already delivered earlier in the same run are kept and charged normally.
- **Shorts and live streams.** A Shorts tile shows only a title and a view count, so Short rows have no date or length (`null`). The Live tab lists streams with "Streamed ... ago" text; only finished streams have been seen on a channel page, so `liveStatus` is `"past"` for those and `null` otherwise - a stream that is live now or scheduled is never guessed. A channel with no Shorts or Live tab simply adds no rows for that tab. When a video's length text is not a length (for example a live stream), `durationSeconds` is `null`.
- **Language and region follow the page YouTube serves.** The Actor asks for English; if YouTube answers in another language the text fields keep that text, and a count that cannot be read from it is `null`.
- **Change mode compares against the last successful run.** The first run delivers everything. A video, Short or stream that merely leaves a tab's first page, or one that is deleted, is not a change. A change that happens and reverts between two runs is not seen.
- Reads about 1 page per 2 seconds, so 1,000 channels take roughly 35 minutes on one tab - and each extra tab adds a page per channel, and each discovered channel a page per tab.
- This is an independent tool, not affiliated with, endorsed by, or connected to YouTube or Google. You are responsible for using the collected data in line with YouTube's terms and applicable law.

### Use it from your code, Make, Zapier or n8n

Built for automated pipelines: small runs, stable field names, one row per item, and you pay only for rows actually delivered.

**One API call that runs the Actor and returns the data** (replace `$APIFY_TOKEN` with your token from Apify Console → Settings → Integrations):

```bash
curl -s -X POST "https://api.apify.com/v2/acts/lukehunter~youtube-stats-tracker/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"channelUrls": ["https://www.youtube.com/@mkbhd", "@veritasium"], "tabs": ["videos"], "maxItems": 20}'
```

**JavaScript** (`npm i apify-client`):

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('lukehunter~youtube-stats-tracker').call({
  "channelUrls": [
    "https://www.youtube.com/@mkbhd",
    "@veritasium"
  ],
  "tabs": [
    "videos"
  ],
  "maxItems": 20
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

**Python** (`pip install apify-client`):

```python
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ['APIFY_TOKEN'])
run = client.actor('lukehunter~youtube-stats-tracker').call(run_input={"channelUrls": ["https://www.youtube.com/@mkbhd", "@veritasium"], "tabs": ["videos"], "maxItems": 20})
items = client.dataset(run['defaultDatasetId']).list_items().items
```

**Schedules, webhooks and no-code tools:** save your input as a Task, add a Schedule (e.g. hourly or daily), and attach a webhook or one of the Integrations tab's connectors (Make, Zapier, n8n, Google Sheets, Slack) to push each run's results into your own system.

**Only new items each run:** set `"onlyChangedSinceLastRun": true` on a scheduled Task and every run after the first delivers, and charges for, only items it hasn't delivered before for that same input. That makes it a ready-made change feed.

**Reliability:** an automated test runs this Actor's parser against the live site every day, and failures are fixed fast.

**Feedback:** an honest rating on the Store page, good or bad, helps others decide if this Actor fits their needs. Missing a field or found a problem? Open an issue on the Actor page.

### FAQ

#### Why can't I paste a video link?

YouTube asks automated clients to sign in to confirm they are not a bot on video pages, and this Actor never bypasses that. Paste the channel's URL or @handle instead: the Videos page lists its latest uploads, and change mode alerts you to new ones.

#### How do I get alerted when a channel posts a new video?

Set `onlyChangedSinceLastRun` to `true` (add `shorts` and `streams` to `tabs` to cover them), save the input as a Task, add a Schedule and connect Slack, Google Sheets or a webhook - see "Alert on new uploads" above. Each run delivers a row with `isNew: true` for every upload it had not seen, and nothing otherwise.

#### Can I get every video on a channel?

No. The Videos tab lists the ~30 newest (the Shorts tab ~48), and going further needs YouTube's paging API, which `robots.txt` disallows. Run on a schedule in change mode and you collect each new upload as it appears.

#### Can I monitor a channel's Shorts and live streams too?

Yes: set `tabs` to `["videos", "shorts", "streams"]` (any combination). Short rows (`type: "short"`) carry the title, link and views; stream rows (`type: "stream"`) the video fields plus `liveStatus`. A Short has no date or length on the channel page, so those are `null`. In change mode each tab is tracked separately, so you are alerted to new Shorts and new streams as well as new videos.

#### How do I find similar channels or build a list of influencers?

Add `"channels"` to `tabs` to get, for each channel you give, the channels it features (one `featuredChannel` row each, with name, @handle, subscribers and video count). Set `discoverDepth` to `1` and each of those channels is also read as a channel row (`discoveredFrom` tells you which seed featured it), so three seeds in a niche become a creator list in one run - see the recipe above. It is the channels that creators themselves feature, not an algorithmic "similar channels" list, and it goes one hop deep.

#### Will discovery keep crawling from channel to channel?

No. `discoverDepth: 1` reads the channels featured by the channels you listed, and stops: the channels those discovered channels feature are never fetched. `maxDiscoveredChannels` (default 20, up to 200) and `maxItems` bound a run further.

#### I switched on a tab for a channel I already watch - will I get 48 "new" alerts?

No. A tab the watch has never seen for that channel is delivered once as its first run (`isNew: null`) and remembered; only items that appear after that are `isNew: true`. Watches created before the `tabs` option keep working unchanged.

#### Are the view and subscriber counts exact?

They are what YouTube shows on the channel page, which rounds them ("21.3M subscribers", "6.1 million views"). The text is kept as shown and the number parsed from it is approximate.

#### Is the first run charged?

Yes, normally: it delivers everything (the channel and the items of the tabs you selected) and remembers it. After that you pay only for new videos, Shorts and streams and for channel rows whose displayed counts changed.

#### What counts as a change?

For videos, Shorts and streams: an ID not seen before for that channel and tab. For channels: the displayed subscriber count, the displayed video count or the status. Views, titles and thumbnails of videos already seen are ignored.

#### What does "blocked" or "consent-required" mean?

YouTube answered with a bot check or a consent page instead of the channel. Nothing was read, nothing was charged, and the run stopped sending requests. See the Limits section.

#### Which links does it accept?

`/@handle`, `/channel/UC...`, `/c/<name>`, `/user/<name>` or a bare `@handle`, with or without a tab. Query strings and tracking parameters are removed, and the same channel listed twice - even as a handle and as a channel ID - is read, and charged, once. Video, search, playlist, feed and YouTube Music links are reported as unsupported.

#### Do I need a YouTube API key?

No. It reads the public channel page, so there is no key and no API quota to manage - and no access to anything that is not on the public page.

#### Can I get YouTube new video alerts in Discord, Telegram, Slack or on my phone?

Yes. Set `discordWebhookUrl`, `telegramBotToken` + `telegramChatId`, `slackWebhookUrl`, `teamsWebhookUrl` or `googleChatWebhookUrl`, or use `ntfyTopic` (free, no account) or `pushoverUserKey` + `pushoverAppToken` for phone pushes. Mattermost and Rocket.Chat work through `slackCompatibleWebhookUrl`. Alerts need change mode (`onlyChangedSinceLastRun`) and a schedule: each run then sends only the new videos, Shorts and streams it delivered (plus channel changes), never the ones you have already seen. Setup steps for each are in the alerts section above. Notifications cost nothing extra.

#### Can I get YouTube new video alerts by email?

Not natively. There are two routes. (1) Set `webhookUrl` to a Zapier, Make or n8n catch hook and add an email or Gmail step: the webhook delivers every row as JSON, so the email can list the updates. (2) Apify's own run integrations and run notifications can email you when a run finishes, but they only send a link to the run or dataset, not the updates themselves. For instant alerts without building anything, Telegram, ntfy or Pushover reach your phone directly.

#### Can I get YouTube new video alerts in WhatsApp?

Not natively, and we do not use unofficial WhatsApp APIs, which risk getting your number banned. The official route is the WhatsApp Business Platform (Cloud API) with its own business account, message templates and pricing: point `webhookUrl` at a Zapier, Make or n8n flow that calls it. For free phone alerts use Telegram or ntfy instead.

### Related Actors

Other data tools from the same developer, built to the same standard: official or public sources, hard cost caps, and honest documentation of limits.

- **[Google Play App Scraper](https://apify.com/lukehunter/google-play-scraper)**: ratings, installs, developer contact info and pricing for any Google Play app.
- **[Apple App Store Reviews Scraper](https://apify.com/lukehunter/app-store-reviews-scraper)**: Apple App Store reviews for any iOS app, across countries, with rating, version and date.
- **[Spotify Scraper](https://apify.com/lukehunter/spotify-scraper)**: play counts, monthly listeners and playlist track lists for any public Spotify artist, playlist, album or track.
- **[Shopify Product Scraper by URL](https://apify.com/lukehunter/shopify-product-tracker)**: price, compare-at price and stock for specific Shopify product URLs, with a change-only feed.
- **[Walmart Category Scraper](https://apify.com/lukehunter/walmart-category-scraper)**: product names, prices, was-prices and ratings from Walmart category pages.
- **[Shopify Store Products Scraper](https://apify.com/lukehunter/shopify-store-products-scraper)**: full product catalogues from any Shopify store, with prices, sale prices, variants and stock.
- **[Vinted Cross-Country Price Comparison](https://apify.com/lukehunter/vinted-cross-country-prices)**: compare the same search across Vinted country markets with EUR-normalized summaries and median price gaps.
- **[Vinted Scraper](https://apify.com/lukehunter/vinted-scraper)**: Vinted search results with prices, brands and sizes in any Vinted country, plus scheduled new-listing alerts sent to Discord or Telegram.

# Actor input Schema

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

REQUIRED. YouTube channels to read, as channel URLs or @handles, e.g. \["https://www.youtube.com/@mkbhd", "@veritasium"]; /channel/UC..., /c/name and /user/name also work. Up to 5,000 per run. Channel pages only: video links (watch, youtu.be, shorts) stop the run with a clear message, because this Actor never bypasses YouTube's sign-in on video pages. A trailing tab (/videos) is ignored; choose tabs below.

## `tabs` (type: `array`):

Which channel tabs to read: "videos" (about 30 latest), "shorts" (about 48), "streams" (past live streams), "channels" (featured channels, for influencer discovery). Default \["videos"]. Each extra tab adds billed rows, e.g. \["videos", "shorts"]. A channel row is always added once per channel. In change mode each tab is tracked separately.

## `discoverDepth` (type: `integer`):

0 (default): only the channels you list. 1: also read the channels they feature, to find similar creators (needs "channels" in tabs, e.g. \["channels"]). One hop, never deeper. Discovered channels are de-duplicated, capped by Maximum discovered channels, marked discoveredFrom, and count toward Maximum rows.

## `maxDiscoveredChannels` (type: `integer`):

Only with discoverDepth 1. Maximum extra channels to read, integer 1-200, default 20, e.g. 10. Each costs one request and at least one billed channel row, so with Maximum rows this is your cost cap. Ignored when depth is 0.

## `output` (type: `string`):

"channelsAndVideos" (default): one channel row plus one row per video, short or stream on the selected tabs. "channelsOnly": just the channel row (subscribers, video count), cheapest for a stats check, e.g. for 200 channels. Every delivered row is one billed result; a channel with 30 videos is up to 31 rows.

## `outputFormat` (type: `string`):

"full" (default): every field. "compact": a much smaller row for AI agents, with numeric counts (subscriberCount, viewCount, durationSeconds), titles, URLs and ids, and no display strings, avatar URLs, timestamps or empty fields. Charging is identical in both formats. Example: "compact" when results are read into an LLM context.

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

Maximum rows returned (channel and video rows together), integer 1-5000, default 100. Also your cost cap: $0.0013 per row. One channel is up to 31 rows with the Videos tab (49 with Shorts). For a quick test use 10, e.g. one channel with maxItems 10.

## `onlyChangedSinceLastRun` (type: `boolean`):

true returns only videos, shorts or streams not seen on a previous scheduled run, plus a channel row when its subscriber or video count changed. Default false: a normal one-off run returns the current rows. Agents making one-off calls should leave it false. Example: run daily on a Schedule with true to get new uploads only; nothing unchanged is charged. The first run delivers everything.

## `stateStoreName` (type: `string`):

Only used when change mode is on. Name of the key-value store that remembers which videos and counts were already seen between scheduled runs, e.g. "youtube-stats-tracker-state". Letters, numbers and hyphens only. Leave the default unless several tasks need separate histories.

## `discordWebhookUrl` (type: `string`):

Post results to a Discord channel. In Discord: Server Settings -> Integrations -> Webhooks -> New Webhook -> choose the channel -> Copy Webhook URL. Example: https://discord.com/api/webhooks/<id>/<token>. Leave empty to skip Discord.

## `telegramBotToken` (type: `string`):

Post results to Telegram. Create a bot by chatting with @BotFather on Telegram (send /newbot) and paste the token it gives you, e.g. 123456789:AA.... Needs "Telegram chat ID" below. Leave empty to skip Telegram.

## `telegramChatId` (type: `string`):

Where the bot should post: a numeric chat id (e.g. 123456789, or -1001234567890 for a group or channel) or a public @channelusername. The bot must be added to that chat or channel first (as an administrator for a channel), and for a private chat you must press Start on the bot. Needs "Telegram bot token" above.

## `slackWebhookUrl` (type: `string`):

Post results to a Slack channel (Block Kit cards, up to 10 per message). Create an incoming webhook at api.slack.com/apps -> Create New App -> Incoming Webhooks -> Add New Webhook to Workspace. Example: https://hooks.slack.com/services/T000/B000/XXXX. Leave empty to skip Slack.

## `slackCompatibleWebhookUrl` (type: `string`):

Post results to a Mattermost or Rocket.Chat incoming webhook, which accept Slack-style webhook payloads. Must be a public https:// address, e.g. https://chat.example.com/hooks/abc123 (no localhost, private IP or .local/.internal hosts). These apps do not render Slack Block Kit, so they get a plain Markdown list. For real Slack use "Slack webhook URL" above. Leave empty to skip.

## `teamsWebhookUrl` (type: `string`):

Post results to a Teams channel as an Adaptive Card. In Teams: channel -> ... -> Workflows -> "Post to a channel when a webhook request is received" -> copy the URL. The host must end in .logic.azure.com or .powerplatform.com, e.g. https://prod-12.westeurope.logic.azure.com/workflows/... The retired Office 365 connector webhooks are not supported. Leave empty to skip Teams.

## `googleChatWebhookUrl` (type: `string`):

Post results to a Google Chat space. In the space: Apps & integrations -> Webhooks -> Add webhook -> copy the URL. Example: https://chat.googleapis.com/v1/spaces/<space>/messages?key=...\&token=.... Leave empty to skip Google Chat.

## `ntfyTopic` (type: `string`):

Get a phone push with no account via ntfy (ntfy.sh). Install the ntfy app, subscribe to a topic name, and enter the same name here. Anyone who knows the topic can read it, so pick a long, unguessable name (letters, numbers, - and \_), e.g. acme-leads-7k2x9q. At most 10 individual pushes per run, then one "...and N more". Leave empty to skip ntfy.

## `ntfyServer` (type: `string`):

Only used with "ntfy topic". Leave as https://ntfy.sh, or enter the public https:// address of your own ntfy server, e.g. https://ntfy.example.com.

## `ntfyToken` (type: `string`):

Only used with "ntfy topic", for a self-hosted server or a reserved topic that needs a login. An access token from ntfy (starts with tk\_, e.g. tk_abc123...). Leave empty for open topics.

## `pushoverUserKey` (type: `string`):

Get a phone push via Pushover. Your user key (30 characters, e.g. uQiRzpo4DXghDmr9QzzfQu27cmVRsG) is shown on the Pushover dashboard (pushover.net). Needs "Pushover app token" below. At most 10 individual pushes per run, then one "...and N more". Leave empty to skip Pushover.

## `pushoverAppToken` (type: `string`):

The API token of a Pushover application you create at pushover.net/apps/build (name it after this Actor), e.g. azGDORePK8gMaC0QOYAMyEEuzJnyUi. Needs "Pushover user key" above.

## `webhookUrl` (type: `string`):

POST the delivered rows as JSON to any public https:// URL: {source, actor, runId, count, items}. At most 100 rows per POST; every delivered row is sent regardless of the max-notifications cap. For example, route results to email, a spreadsheet or any app. Localhost, private IP and .local/.internal addresses are rejected. Leave empty to skip.

## `maxNotifications` (type: `integer`):

Caps how many results are sent to chat and phone channels per run (Discord, Telegram, Slack, Teams, Google Chat, ntfy, Pushover), 1-100, default 25 (e.g. 10). Extra results are still in the dataset. The generic webhook is not capped. Only used when a destination above is set.

## Actor input object example

```json
{
  "channelUrls": [
    "https://www.youtube.com/@mkbhd",
    "@veritasium"
  ],
  "tabs": [
    "videos"
  ],
  "discoverDepth": 0,
  "maxDiscoveredChannels": 20,
  "output": "channelsAndVideos",
  "outputFormat": "full",
  "maxItems": 100,
  "onlyChangedSinceLastRun": false,
  "stateStoreName": "youtube-stats-tracker-state",
  "ntfyServer": "https://ntfy.sh",
  "maxNotifications": 25
}
```

# Actor output Schema

## `channels` (type: `string`):

No description

## `videos` (type: `string`):

No description

## `shorts` (type: `string`):

No description

## `streams` (type: `string`):

No description

## `featuredChannels` (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 = {
    "channelUrls": [
        "https://www.youtube.com/@mkbhd",
        "@veritasium"
    ],
    "tabs": [
        "videos"
    ],
    "maxItems": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("lukehunter/youtube-stats-tracker").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/@mkbhd",
        "@veritasium",
    ],
    "tabs": ["videos"],
    "maxItems": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("lukehunter/youtube-stats-tracker").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/@mkbhd",
    "@veritasium"
  ],
  "tabs": [
    "videos"
  ],
  "maxItems": 100
}' |
apify call lukehunter/youtube-stats-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,lukehunter/youtube-stats-tracker"
        }
    }
}
```

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/Y7cPwGMFKDVT2WaLG/builds/ZFD9dtN8jN1tkhO5O/openapi.json
