# YouTube Monitor (`reportable_broth/youtube-scraper-monitor`) Actor

YouTube scraper with no API key and no quota limits. Extract new videos by keyword or channel: title, channel, views, duration, publish time, thumbnail and URL. Export JSON, CSV or Excel. Built-in dedupe returns only what is new since your last run. $1.99 per 1,000 videos.

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

## Pricing

from $1.99 / 1,000 videos

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 Keyword Monitor — New Videos Only | $1.99 / 1K videos | No API Key

Watch YouTube for **new** videos on your keywords, or from the channels you follow — and get **only what appeared since your last run**.

**No API key. No quota. No login. No cookies.** This Actor reads public YouTube pages, so there is no Google Cloud project to set up, no daily quota to run out of, and no account to get flagged. If you have hit the YouTube Data API's daily unit limit, this is a drop-in alternative that does not have one.

**$1.99 per 1,000 videos returned. Platform usage included — no second bill for compute or proxy.**

***

### This is a monitor, not a bulk scraper

Most YouTube scrapers are built to dump everything: give them a channel, get 20,000 videos back. That is a different job.

This one is built to run **on a schedule** and answer one question: *what is new?*

| | This Actor | A bulk scraper |
|---|---|---|
| Built for | Running every 15 min, forever | One big extraction |
| Returns | **Only videos you have not seen** | Everything, every time |
| Repeat cost | You pay for new videos only | You pay for the same videos again |
| Dedupe | **Built in, 7-day memory** | You do it yourself |
| Typical run | A handful of new videos | Thousands of rows |

If you want a one-off bulk dump, use a bulk scraper. If you want an alerting feed, this is it.

***

### What you can do with it

- **Brand and competitor monitoring** — know within minutes when someone uploads a video mentioning your product
- **Trend watching** — track a topic and collect every new video about it over weeks
- **Creator tracking** — follow a list of channels and get their uploads as they land
- **Lead and partner discovery** — find the channels publishing about your niche
- **AI agents and RAG** — a clean, deduplicated feed of fresh video metadata
- **Alerting** — pipe new videos into Slack, Discord, email or a webhook

***

### Two inputs, mix freely

#### Keywords

```json
{
  "keywords": ["ai agents", "web scraping"],
  "sort": "date",
  "onlyNew": true
}
```

`sort: date` puts newest uploads first, which is what monitoring needs. `relevance`, `views` and `rating` are also available when you want ranking instead of recency.

#### Channels

```json
{
  "channels": ["@NASA", "@veritasium"],
  "onlyNew": true
}
```

Handles work with or without the `@`, and full channel URLs are accepted too.

#### Both at once

```json
{
  "keywords": ["mars rover"],
  "channels": ["@NASA"],
  "sort": "date",
  "onlyNew": true,
  "maxAgeHours": 48
}
```

Every item is tagged with `_source` (`search` or `channel`) and `_query` so you can split them downstream.

***

### Ready-made examples

**Brand monitoring, every 15 minutes**

```json
{
  "keywords": ["acme corp", "acme review"],
  "sort": "date",
  "onlyNew": true,
  "maxAgeHours": 24
}
```

**Follow a set of creators**

```json
{
  "channels": ["@mkbhd", "@LinusTechTips", "@veritasium"],
  "onlyNew": true
}
```

**Only videos that already have traction**

```json
{
  "keywords": ["product launch 2026"],
  "sort": "views",
  "minViews": 10000,
  "onlyNew": true
}
```

**Full snapshot, no dedupe (one-off research)**

```json
{
  "keywords": ["electric vehicle review"],
  "sort": "relevance",
  "onlyNew": false
}
```

**Feed an AI agent**

```json
{
  "keywords": ["the topic your agent is asking about"],
  "sort": "date",
  "onlyNew": false,
  "maxAgeHours": 24
}
```

***

### Input reference

| Field | Type | Default | What it does |
|---|---|---|---|
| `keywords` | array | — | Search terms to monitor. |
| `channels` | array | — | Channel handles or URLs to monitor. |
| `sort` | string | `date` | `date` (newest, for monitoring), `relevance`, `views`, `rating`. |
| `onlyNew` | boolean | `true` | Return only videos not seen in previous runs. 7-day memory per input set. |
| `maxAgeHours` | integer | `0` | Skip videos older than this. `0` = no limit. Approximate — see below. |
| `minViews` | integer | `0` | Skip videos below this view count. |
| `maxResults` | integer | `0` | Videos per keyword / channel. `0` = first page only. Higher values page through YouTube. |
| `includeShorts` | boolean | `true` | Shorts are often more than half of a keyword's results. Turn off for long-form only. |
| `filterLanguages` | array | `[]` | Keep only titles written in these languages. Undetermined titles are kept. |
| `enrichVideos` | boolean | `false` | Open each video for its exact publish time, like count, full description and keywords. One extra request per video. |
| `transcripts` | boolean | `false` | Add the spoken transcript of each video. |
| `transcriptLanguages` | array | `["en"]` | Preferred transcript languages, tried in order. |
| `proxyConfiguration` | object | Apify residential | Residential recommended. |
| `concurrency` | integer | `3` | How many keywords/channels to fetch at once. |

***

### Output

One dataset item per video. Export as **JSON, CSV, Excel or XML**, or pull it straight from the Apify API.

| Field | Type | Notes |
|---|---|---|
| `video_id` | string | YouTube video ID |
| `url` | string | Direct watch link |
| `title` | string | |
| `channel` | string | Channel name |
| `channel_url` | string or null | Filled for both keyword and channel runs |
| `duration_text` | string or null | e.g. `14:05`. Blank for a live stream. |
| `view_count` | integer or null | Parsed to a number |
| `view_count_text` | string or null | As YouTube shows it, e.g. `486K views` |
| `published_text` | string or null | As YouTube shows it, e.g. `12 days ago` |
| `published_ts_approx` | integer or null | **Approximate** Unix timestamp — see below |
| `description_snippet` | string or null | Present for keyword search only — YouTube does not render descriptions on a channel's video grid |
| `thumbnail` | string or null | Highest-resolution thumbnail URL |
| `is_short` | boolean | Whether this is a YouTube Short |
| `language` | string or null | Detected language of the title, ISO 639-1. `null` when it cannot be determined. |
| `published_at` | string or null | **Exact** ISO 8601 publish time. Only with `enrichVideos`. |
| `published_ts` | integer or null | Exact Unix timestamp. Only with `enrichVideos`. |
| `like_count` | integer or null | Only with `enrichVideos`. |
| `description` | string or null | Full description. Only with `enrichVideos`. |
| `keywords` | array | Video tags. Only with `enrichVideos`. |
| `channel_id` | string or null | Only with `enrichVideos`. |
| `subscriber_count` | integer or null | The channel's subscriber count, read from the video owner block. YouTube omits that block on some page variants, so this is `null` more often than the other fields — it is left empty rather than filled from a recommended channel. Only with `enrichVideos`. |
| `channel_thumbnail` | string or null | The channel's avatar. |
| `channel_username` | string or null | The channel's @handle. |
| `comment_count` | integer or null | Exact count. Only with `exactCommentCount`. |
| `comment_count_approx` | integer or null | **Rounded** — YouTube only shows `4.2K` on the video page. Only with `enrichVideos`. |
| `hashtags` | array | Hashtags parsed from the description. Only with `enrichVideos`. |
| `description_links` | array | Links parsed from the description. Only with `enrichVideos`. |
| `duration_seconds` | integer or null | Only with `enrichVideos`. |
| `transcript` | string or null | Full spoken transcript. Only with `transcripts`. |
| `transcript_status` | string | Why a transcript is missing: `ok`, `disabled_by_uploader`, `no_transcript`, `video_unavailable`, `blocked`, `error`. |
| `transcript_segments` | array | Transcript with `start`, `duration` and `text` per line. Only with `transcripts`. |
| `transcript_language` | string or null | Language actually returned. |
| `transcript_is_generated` | boolean | Whether YouTube auto-generated it, as opposed to a human-written one. |
| `_source` | string | `search` or `channel` |
| `_query` | string | The keyword or channel that produced this item |
| `_mode` | string | The sort mode, or `videos` for channels |

#### Sample record

```json
{
  "video_id": "IwZVXmQdX1E",
  "url": "https://www.youtube.com/watch?v=IwZVXmQdX1E",
  "title": "NASA Moon Base: The First Six Months",
  "channel": "NASA",
  "channel_url": "https://www.youtube.com/@NASA",
  "duration_text": "14:05",
  "view_count": 486000,
  "view_count_text": "486K views",
  "published_text": "12 days ago",
  "published_ts_approx": 1757600000,
  "thumbnail": "https://i.ytimg.com/vi/IwZVXmQdX1E/hq720.jpg",
  "_source": "channel",
  "_query": "NASA",
  "_mode": "videos"
}
```

***

### Turn it into a live monitor

1. Run once with your keywords and check the output.
2. Save it as a Task.
3. Add a Schedule — every 15 minutes works well.
4. Keep `onlyNew` on, so each run emits only videos you have not seen.
5. Connect the dataset to Slack, Discord, a webhook, Google Sheets, Make or Zapier.

You now have a YouTube alerting feed that never repeats itself.

### Use it from an AI agent

Callable from the Apify API and over MCP. Set `onlyNew` to `false` for agent calls so each question gets a full answer rather than a delta.

***

### What to expect

**About timestamps — read this one.** A search or channel listing shows only relative text like `12 days ago`, never an exact time. The video's own page does carry an exact timestamp, and `enrichVideos` fetches it — so if you need real timestamps, turn that on. This Actor gives you that text verbatim in `published_text`, and converts it to an approximate Unix timestamp in `published_ts_approx`. The field is named `approx` on purpose. It is accurate enough to answer "in the last 24 hours" and not accurate enough to sort videos uploaded minutes apart.

**Shorts carry no publish time at all.** YouTube's Shorts component does not include one, so `published_text`, `published_ts_approx` and `duration_text` are always `null` for a Short. Since videos without a timestamp are kept rather than dropped, `maxAgeHours` does not constrain Shorts — set `includeShorts` to `false` if you need the age filter to apply to everything you receive.

Without `enrichVideos`, `maxAgeHours` is therefore an approximate filter. Videos with no readable publish time are **kept**, not dropped — you should not lose a video because YouTube was vague about its age.

**How many results per run.** With `maxResults` at 0, a keyword search returns roughly 15 to 50 videos depending on how many Shorts YouTube mixes in, and a channel returns its latest ~30 uploads. Raise `maxResults` to go deeper. With `onlyNew` on, a scheduled run typically returns far fewer — which is the point.

**Why a run can return few or zero items.** In order of likelihood:

1. `onlyNew` is on and nothing new has been uploaded. This is correct behaviour, not a failure.
2. `maxAgeHours` or `minViews` filtered everything out.
3. The keyword is genuinely quiet.
4. The channel handle is wrong or the channel has no public uploads.

**Public content only.** Private, unlisted and members-only videos are not accessible.

***

### FAQ

**Do I need a YouTube Data API key?**
No. That is the main reason to use this. No Google Cloud project, no quota units, no daily limit to exhaust.

**Do I need to log in or supply cookies?**
No. The Actor reads public pages only.

**Can my Google account get banned?**
There is no account involved, so there is nothing to ban.

**How is this different from a YouTube scraper?**
A scraper dumps everything every time. This one remembers what it already gave you and returns only new videos. That makes it cheap to run every 15 minutes, and it means you are not charged twice for the same video.

**Am I charged for videos I already received?**
No. With `onlyNew` on, videos you have seen are filtered before they reach the dataset, so they are never charged.

**My second run returned almost nothing. Is it broken?**
Almost certainly not — that is `onlyNew` working. Set it to `false` if you want the full list every time.

**How far back does the memory go?**
7 days per input set. Change the keywords or channels and it starts a fresh memory.

**How many videos actually have a transcript?**
Across 172 videos from four keywords, about 60% did. The rest is not a failure on this Actor's side: uploaders can switch captions off, and most Shorts have them off. Every video carries a `transcript_status` saying which it was, so an empty transcript is never ambiguous. Turn `includeShorts` off and the share rises sharply.

**Does it get transcripts?**
Yes — set `transcripts` to `true`. If a video has no transcript in your preferred languages, whatever transcript it does have is returned rather than nothing, and a human-written one is preferred over an auto-generated one where there is a choice.

**Does it get comments?**
Not in this version.

**Does it include Shorts?**
Yes, and they are flagged with `is_short` so you can split or drop them. For many keywords Shorts are more than half of what YouTube returns — a search for `skincare` came back with 38 Shorts and 8 long-form videos. Set `includeShorts` to `false` for long-form only.

**Can I filter by date?**
Use `maxAgeHours`. Read the timestamp note in *What to expect* first — it is approximate by necessity.

**Which proxy should I use?**
Residential, which is the default. YouTube serves a stripped page to flagged IPs rather than returning an error, so a clean IP matters.

**Can I use my own proxies?**
Yes, through `proxyConfiguration`.

***

### Changelog

- **0.4.0** — Transcripts. Full text and timed segments, with a language fallback so a video is not skipped just because it has no English transcript.
- **0.3.0** — `enrichVideos` adds the exact publish timestamp, subscriber count, rounded comment count, hashtags, description links, like count, full description, keywords and channel ID by opening each video. The note claiming YouTube never exposes an exact upload time was wrong and has been corrected.
- **0.2.0** — Shorts are now returned and flagged with `is_short` (they were silently dropped before, which cost a keyword search most of its results). `maxResults` pages past the first page. Every video carries a detected `language`, and `filterLanguages` keeps only the languages you want.
- **0.1.1** — Channel runs now return `duration_text`, and view counts and publish times survive YouTube's compact metadata layout (previously null on roughly half of all channel runs).
- **0.1.0** — First release. Keyword and channel monitoring, cross-run dedupe, age and view filters.

***

### Notes and limits

- Reads **public** YouTube pages only.
- By default a keyword search returns roughly the first page and a channel its latest uploads. Raise `maxResults` to page further — a channel's back catalogue keeps going for hundreds of videos.
- YouTube changes its page structure regularly. This Actor parses several of YouTube's video components and is maintained against those changes. Run a small sample first, check the output, then scale up.
- Timestamps are approximate. See *What to expect*.

***

### Our other Actors

- **[Threads Scraper](https://apify.com/reportable_broth/threads-scraper-monitor)** — search Threads by keyword or pull a profile's posts, replies, reposts and media. No login, no cookies.
- **[YouTube Transcript Scraper](https://apify.com/reportable_broth/youtube-transcript-scraper)** — the spoken transcript of any video, by URL, channel or keyword. Falls back to any available language instead of failing.

### Disclaimer

This Actor collects only publicly available data. You are responsible for how you use the data you collect, including compliance with YouTube's terms of service, applicable data protection law such as GDPR, and any restrictions that apply in your jurisdiction. If you are unsure, take legal advice before scraping personal data at scale.

***

**Keywords:** youtube scraper, youtube monitor, youtube data api alternative, youtube without api key, no quota, youtube keyword alerts, youtube new video alerts, youtube channel scraper, youtube search scraper, extract youtube videos, export to csv, export to excel, json output, video monitoring, social listening, brand monitoring, youtube data extraction.

# Actor input Schema

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

Search terms or topics. Each one is searched on YouTube. Use the Sort option to control what 'top of the list' means.

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

Channel handles (with or without @) or channel URLs. Returns that channel's latest uploads.

## `sort` (type: `string`):

date = newest uploads first, which is what you want for monitoring. relevance = YouTube's default ranking. views / rating sort by popularity.

## `maxResults` (type: `integer`):

How many videos to return for each keyword and each channel. Leave at 0 for the first page only (about 15 for a keyword, 30 for a channel). Higher values page through YouTube — a channel's back catalogue keeps going for hundreds of videos.

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

Shorts often make up more than half of a keyword's results. Turn this off to get long-form videos only.

## `filterLanguages` (type: `array`):

Keep only videos whose title is written in one of these languages. Leave empty for everything. Videos whose language cannot be determined are kept, not dropped.

## `enrichVideos` (type: `boolean`):

Off by default. When on, each video is opened to add its **exact** publish timestamp, like count, full description, keywords, channel ID and exact view count — data that a search or channel listing does not carry. Costs one extra request per video, so runs take longer.

## `exactCommentCount` (type: `boolean`):

Off by default. The video page only shows a rounded count like `4.2K`; turning this on fetches the exact number (4,296) at the cost of one extra request per video.

## `transcripts` (type: `boolean`):

Off by default. Adds the spoken transcript of each video, as full text and as timed segments. If a video has no transcript in your preferred languages, any available transcript is returned instead of nothing — a human-written one where there is a choice.

## `transcriptLanguages` (type: `array`):

Tried in order. A video with none of them still returns whatever transcript it has.

## `onlyNew` (type: `boolean`):

Keeps a 7-day memory of video IDs per input set. Turn on for scheduled monitoring so every run yields only NEW videos. Turn off to always get the full list.

## `maxAgeHours` (type: `integer`):

0 = no limit. Note: YouTube only publishes relative times such as '12 days ago', so this filter is approximate. Videos with no readable publish time are kept rather than dropped.

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

Skip videos below this view count. 0 = no filter.

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

Residential proxy is recommended. YouTube serves a stripped page to flagged IPs instead of returning an error.

## `concurrency` (type: `integer`):

How many keywords/channels to fetch at once. Raise carefully.

## Actor input object example

```json
{
  "keywords": [
    "ai agents",
    "web scraping"
  ],
  "channels": [
    "@NASA"
  ],
  "sort": "date",
  "maxResults": 0,
  "includeShorts": true,
  "filterLanguages": [],
  "enrichVideos": false,
  "exactCommentCount": false,
  "transcripts": false,
  "transcriptLanguages": [
    "en"
  ],
  "onlyNew": true,
  "maxAgeHours": 0,
  "minViews": 0,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "concurrency": 3
}
```

# Actor output Schema

## `videos` (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 = {
    "keywords": [
        "ai agents",
        "web scraping"
    ],
    "channels": [
        "@NASA"
    ],
    "sort": "date",
    "maxResults": 0,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("reportable_broth/youtube-scraper-monitor").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 = {
    "keywords": [
        "ai agents",
        "web scraping",
    ],
    "channels": ["@NASA"],
    "sort": "date",
    "maxResults": 0,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("reportable_broth/youtube-scraper-monitor").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 '{
  "keywords": [
    "ai agents",
    "web scraping"
  ],
  "channels": [
    "@NASA"
  ],
  "sort": "date",
  "maxResults": 0,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call reportable_broth/youtube-scraper-monitor --silent --output-dataset

```

## MCP server setup

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

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/boA20a8FShwoYw5Vh/builds/bJDYiw7YVjEJV4OMP/openapi.json
