# YouTube Community Posts Scraper & New Post Monitor (`neverempty/youtube-community-posts-scraper`) Actor

Posts from any YouTube channel's Posts (Community) tab: text, full links, images, polls with vote totals, quizzes with correct answers, shared videos, likes and comments. Monitoring mode returns only posts that are new since the last run, and can track poll vote counts.

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

## Pricing

from $2.40 / 1,000 post returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

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 Community Posts Scraper & New Post Monitor

Posts from any YouTube channel's **Posts** tab (formerly the Community tab): the post text with its **full links**, images, polls with their vote totals, quizzes with the correct answer and explanation, shared videos and playlists, likes and comments. With monitoring on, **a scheduled run returns only the posts that are new since the last run**, and can also return a poll again when its vote count moves.

Paste channels in any form, mixed in one list: `@handle`, a plain handle, a `UC...` channel id, a channel URL (`/@name`, `/channel/UC...`, `/c/Name`, `/user/Name`), or **a video URL** - for a video, the channel that uploaded it is read.

### What you get for each post

| Column | What it is |
|---|---|
| `postId`, `postUrl` | The post's id and its link (`https://www.youtube.com/post/...`) |
| `postType` | `text`, `image`, `poll`, `quiz`, `video` or `playlist` |
| `text` | The post text as YouTube displays it |
| `links` | The real URLs linked in the text. YouTube shortens long links in the displayed text ("https://sept11documents.cityofnewyork..."); `links` has the full address (`https://sept11documents.cityofnewyork.us/`) |
| `publishedTimeText`, `publishedAgoValue`, `publishedAgoUnit`, `publishedMinAgeDays`, `isEdited` | YouTube shows only a relative time ("13 days ago", "2 years ago (edited)"). It is split into its number and unit, and `publishedMinAgeDays` counts it as days (a week as 7, a month as 30, a year as 365). No calendar date is invented. |
| `likeCount`, `likeCountText`, `likeCountIsRounded` | Likes as displayed ("101K likes" is 101000, flagged as rounded) |
| `commentCount`, `commentCountText`, `commentCountIsRounded` | Number of comments as displayed |
| `images`, `imageCount` | The largest image URL YouTube offers for each image of the post |
| `pollChoices`, `pollType` | The choices of a poll (`text` or `image` poll) |
| `quizChoices` | Each quiz choice with `isCorrect` and the `explanation` YouTube shows after answering |
| `voteCount`, `voteCountText`, `voteCountIsRounded` | The total votes of a poll ("1.7M votes") or answers of a quiz ("330K answered") |
| `videoId`, `videoUrl`, `videoTitle`, `videoLengthText`, `videoViewCount`, `videoPublishedTimeText`, `videoChannelId`, `videoChannelTitle`, `videoIsMembersOnly` | The video shared in the post (it can belong to another channel) |
| `playlistId`, `playlistTitle`, `playlistVideoCount` | The playlist shared in the post |
| `channelId`, `channelTitle`, `handle`, `channelUrl` | The channel whose Posts tab was read |
| `input`, `inputType`, `resolvedFromVideoId`, `channelMatchesInput` | What you typed, and whether the channel YouTube returned is the handle or id you typed (`null` for `/c/`, `/user/` and video URLs) |
| `returnedBecause`, `previousVoteCount`, `voteCountDelta`, `voteCountDeltaIsApproximate`, `previousCheckedAt` | Monitoring only: why the post came back, and the vote count change of a poll |

**Poll percentages are not in the output.** YouTube shows the share of each choice only to signed-in users who have voted; the public page carries the choices and the rounded total only.

Measured on 2026-09-14 on 156 posts from 7 channels (MrBeast, Veritasium, TED-Ed, PewDiePie, Markiplier, Marques Brownlee and YouTube's Music channel):

| postType | Posts |
|---|---|
| image | 97 |
| text | 24 |
| video | 20 |
| poll | 9 |
| quiz | 4 |
| playlist | 2 |

153 of the 156 like counts were abbreviated by YouTube (for example "3.4M"), which is why every count comes with its original text and a rounded flag.

### Why every post is checked against the channel

A YouTube channel page also carries other channels' ids and names. This Actor reads the channel's own id from the page's metadata, and returns a post only when the channel that wrote it is that same id; posts written by another channel are not returned. Every row states the channel it came from (`channelId`, `handle`, `channelTitle`), and the run log prints the name each input resolved to.

### How many posts are read

- The Posts page of a channel carries its newest posts (6 to 10 per channel in the measurements of 2026-09-14). Older posts are loaded 10 at a time, the same way YouTube's website loads them when you scroll, until `maxPostsPerChannel` posts are read (up to 1,000) or YouTube has no more. On 2026-09-14, MrBeast's Posts tab ended at 84 posts.
- When a channel has more posts than `maxPostsPerChannel`, a free `more-posts-available` row names it. Posts beyond the limit are not read and not charged.
- Loading the older posts uses `youtube.com/youtubei/v1/browse`, which YouTube's robots.txt disallows for crawlers. The newest posts on a channel's Posts page do not need it: when `maxPostsPerChannel` is not more than the posts on that first page, or when a monitoring run finds a post seen last time on it, only the Posts page is requested.
- Filters (`postTypes`, `maxPostAgeDays`, `keywords`, `excludeKeywords`) are applied to the posts read. With `maxPostAgeDays`, once every post on a page is older than the limit, older pages are not requested.

### Monitoring mode

Turn **monitoringMode** on and schedule the Actor. It remembers which posts each channel had and, on later runs:

- only posts published since the last run are returned, **oldest first**, with `returnedBecause: "new-post"`;
- a run reads each channel's posts until it reaches a post seen last time. `maxPostsPerChannel` does not cut the new posts: if 40 posts appeared since the last run, all 40 are returned. It is used only on the first run for a channel, to decide how many of the newest posts to read (and return) that first time. If none of the posts seen last time is on the Posts tab any more (they were deleted, or 100 pages - about 1,000 posts - were read without reaching them), new posts cannot be told apart from older ones: that channel returns nothing and is not charged (not even the check), the posts now on the tab are remembered as the new starting point, and a `monitoring-restarted` row says so;
- a channel with no new post is **not returned as a row**; it costs the check fee (see below), and if no channel has a new post you get one free `no-new-posts` row;
- if YouTube stops answering before the run reaches the posts seen last time, that channel returns nothing and is not charged at all (not even the check), and an `incomplete` row names it, so no new post is skipped.

Turn on **trackPollVotes** as well to return a poll or quiz seen on an earlier run again whenever the total vote count YouTube displays has changed, with `returnedBecause: "vote-count-changed"`, `previousVoteCount`, `voteCountDelta` and `previousCheckedAt`. It compares the polls among the posts read in that run, which are at least each channel's newest `maxPostsPerChannel` posts. YouTube displays the total rounded ("1.7M votes"), so `voteCountDeltaIsApproximate` is `true` when either side was abbreviated, and a large poll can show the same figure for a long time.

Posts are remembered **per channel, not per list**, in a store in your own account, so adding a channel to the list does not re-charge the others. Posts dropped by your filters are remembered too, so removing a filter later does not return old posts as new. **resetMonitoringState** clears everything that was remembered. Do not leave it on in a schedule: every run would then start over and return and charge all posts again as a first monitoring run (a `monitoring-memory-cleared` row warns about this).

To cap what a monitoring run can cost, set the run's maximum total charge. Before reading each channel, the run makes sure the maximum leaves room for that channel's check plus one post row, so a low limit does not keep charging checks without returning anything: channels that did not fit are not read, not charged, and named in a free `not-read` row. If the limit runs out in the middle of a channel's new posts, the older ones are returned and remembered, and the newer ones are not remembered, so a later run with enough room returns them.

**Do not put the same channel in two monitoring schedules that can run at the same time.** Each run re-reads what was remembered just before saving and adds only what it saw itself, so overlapping runs keep each other's posts. But Apify's key-value store has no atomic update, so two runs saving the same channel at the same moment can still overwrite each other, and a post may then be returned once more on a later run.

### What it costs

- **Post returned**: $3.00 per 1,000 posts. With monitoring off, that is the only charge: one per post row you receive.
- **Channel checked** (monitoring mode only): $0.30 per 1,000 checks - one per channel whose posts were read and compared, whether or not it had a new post. This pays for watching on the days when nothing was posted.
- The rows that explain a missing channel, a missing Posts tab, a duplicate, "no new posts", "no filter match", more available posts or an unread channel are free. A channel that could not be read is not charged as a check.

Example: 100 channels checked once a day is about 3,000 checks a month, $0.90, plus $3.00 per 1,000 for the new posts returned.

### Input

| Field | Meaning |
|---|---|
| `channels` | One channel per line: `@handle`, handle, `UC...` id, channel URL or video URL. Up to 1,000 lines per run. The same channel given twice is read and charged once. Empty = `@MrBeast` as an example, and a row says so. |
| `maxPostsPerChannel` | How many of each channel's newest posts to read (1-1,000, default 50). In monitoring mode, used only on a channel's first run and for poll tracking. |
| `postTypes` | Keep only `text`, `image`, `poll`, `quiz`, `video` or `playlist` posts. Empty = all. |
| `maxPostAgeDays` | Keep only posts whose displayed age, counted as days, is at most this. 0 = no limit. |
| `keywords`, `keywordMatch`, `excludeKeywords` | Keep or drop posts by words in the text, poll choices, shared video title or shared playlist title. |
| `monitoringMode` | Return only posts that are new since the last run (see above). |
| `trackPollVotes` | Monitoring only: also return polls and quizzes whose displayed vote count changed. |
| `resetMonitoringState` | Forget every remembered post and vote count before the run. |
| `useProxy` | Pages are requested directly first; on a bot check, HTTP 429 or a page without the expected data, the run retries through a proxy session (up to 3 attempts). |

### Rows that are not posts

Every row has a `status`. Posts are `ok`. The others explain why there is no post, and are never charged:

| status | Meaning |
|---|---|
| `no-such-channel` | YouTube answered HTTP 404 for this channel address. |
| `no-such-video` | The video URL did not resolve to a video, so its channel could not be found. |
| `invalid-input` | The line is not a handle, channel id, channel URL or video URL (search, playlist, hashtag and single post pages are not read). |
| `no-posts-tab` | YouTube opened another tab of the channel instead of a Posts tab, so there are no posts to read. The row does not claim why. |
| `no-results` | The Posts tab loaded but lists no posts. |
| `blocked` | Every attempt came back with a bot check. |
| `unreadable` | The page loaded but did not contain what was expected; the reason is in the note. |
| `incomplete` | A later page of the channel's posts could not be read. |
| `monitoring-restarted` | Monitoring: none of the posts remembered last time is on the channel's Posts tab any more, so nothing was returned or charged for it and the posts now on the tab were remembered as the new starting point. |
| `monitoring-memory-cleared` | resetMonitoringState was on, so everything remembered was cleared before the run. Leaving it on in a schedule returns and charges all posts again on every run. |
| `duplicate` | The line points to a channel already read in this run. |
| `no-new-posts` | Monitoring: no channel had a new post (or a changed poll, with trackPollVotes). The note says how many checks were charged. |
| `no-match` | Posts were read but none matched your filters. |
| `more-posts-available` | These channels have more posts than `maxPostsPerChannel`. |
| `budget-reached` | The run's maximum total charge was reached; the posts that did not fit are not in the dataset and were not charged. |
| `not-read` | Channels that were not read because the maximum total charge left no room, or because a run reads at most 1,000 lines. |
| `example-input` | No channel was given, so `@MrBeast` was read as an example. |

### Good to know

- Only public posts are read, without signing in. Members-only content is not unlocked; a shared members-only video is flagged in `videoIsMembersOnly`.
- The numbers are YouTube's displayed figures at the time of the run. `scrapedAt` says when.
- Values YouTube does not show are `null`, never 0 (for example `videoViewCount` of a members-only video).
- E-mail addresses and phone numbers written into a post's text or poll choices are replaced with `[email removed]` / `[phone removed]`. The URLs in `links` are returned as they are.
- The first page of each channel is requested directly; a proxy is used only when YouTube answers with a bot check, which did not happen in the measurements of 2026-09-14.

# Actor input Schema

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

One channel per line, in any of these forms, mixed freely: @handle (@MrBeast), a plain handle (MrBeast), a channel id (UC...), a channel URL (https://www.youtube.com/@MrBeast, /channel/UC..., /c/Name, /user/Name, with or without https://), or a video URL (watch?v=, youtu.be, /shorts/) - for a video, the channel that uploaded it is read. Every row names the channel YouTube actually returned (channelId, handle, channelTitle) and says in channelMatchesInput whether it is the handle or id you typed. The same channel given twice is read and charged once. Up to 1,000 lines per run. If left empty, @MrBeast is read as an example and the output says so.

## `maxPostsPerChannel` (type: `integer`):

How many of each channel's newest posts to read (1 to 1,000). Filters are applied to these posts, and you are charged only for the post rows you receive. When a channel has more posts than this, a free more-posts-available row names it. In monitoring mode this is used only on the first run for a channel (and, with poll tracking on, as the number of newest posts whose vote counts are compared); later runs read until they reach the posts seen last time.

## `postTypes` (type: `array`):

Keep only these kinds of post. text = no attachment, image = one or more images, poll = a poll, quiz = a quiz, video = a shared video, playlist = a shared playlist. Empty = every kind.

## `maxPostAgeDays` (type: `integer`):

YouTube shows only a relative time ("13 days ago", "1 month ago", "2 years ago"), so this compares the displayed age counted as days (a week as 7, a month as 30, a year as 365): "1 month ago" counts as 30 days even if the post is 50 days old. When every post on a page is older than this, older pages are not read. A post whose age cannot be read is kept. 0 = no limit.

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

Keep only posts whose text, poll choices, shared video title or shared playlist title contain these words.

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

Any = at least one keyword. All = every keyword.

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

Drop posts whose text, poll choices, shared video title or shared playlist title contain any of these words.

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

Off = return each channel's newest posts, charged per post row. On = remember which posts each channel had and, on later runs, return only posts published since then (oldest first). Each channel checked costs a small per-check fee ($0.30 per 1,000 checks) whether or not it has a new post; new posts are also charged as post rows. Posts are remembered per channel, not per list, so adding a channel to the list does not re-charge the others. Do not put the same channel in two monitoring schedules that can run at the same time: overlapping runs keep each other's channels, but Apify's key-value store has no atomic update, so the same channel saved by both at once can return a post again later.

## `trackPollVotes` (type: `boolean`):

Monitoring mode only. Among each channel's newest posts (up to maxPostsPerChannel), a poll or quiz seen on an earlier run is returned again, and charged as a post row, when the total vote count YouTube displays has changed, with previousVoteCount and voteCountDelta. YouTube displays the total rounded (1.7M votes), so a large poll can show the same figure for a long time, and the share of each choice is shown only to signed-in users who voted, so it is not in the output.

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

Clears every remembered post and vote count for this Actor in your account before the run, so the next monitoring run treats every channel as new again. This affects all your monitoring runs, because posts are stored per channel rather than per list.

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

Every page is requested directly first, which is the fastest and cheapest path and worked from Apify on every attempt when measured on 2026-09-14. If YouTube answers with a bot check, HTTP 429 or a page without the expected data, the run switches to a proxy session and tries again (up to 3 attempts). With this off, such a page is returned as an uncharged row instead.

## Actor input object example

```json
{
  "channels": [
    "@MrBeast",
    "@veritasium",
    "https://www.youtube.com/@TEDEd"
  ],
  "maxPostsPerChannel": 50,
  "maxPostAgeDays": 0,
  "keywordMatch": "any",
  "monitoringMode": false,
  "trackPollVotes": false,
  "resetMonitoringState": false,
  "useProxy": true
}
```

# Actor output Schema

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

One row per YouTube community post with its text, links, images, poll or quiz, shared video or playlist, likes and comments, or an uncharged row saying why there is none (no such channel, no Posts tab, no posts, bot check, no new posts since the last monitoring run, no filter match, duplicate, incomplete read).

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "channels": [
        "@MrBeast",
        "@veritasium",
        "https://www.youtube.com/@TEDEd"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/youtube-community-posts-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "channels": [
        "@MrBeast",
        "@veritasium",
        "https://www.youtube.com/@TEDEd",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/youtube-community-posts-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "channels": [
    "@MrBeast",
    "@veritasium",
    "https://www.youtube.com/@TEDEd"
  ]
}' |
apify call neverempty/youtube-community-posts-scraper --silent --output-dataset

```

## MCP server setup

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

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/bqwLlGHC6Wsb9DAxQ/builds/5sfMKR2hwqmUAFDJb/openapi.json
