# YouTube Comments Scraper - Comments, Replies, Likes & Authors (`neverempty/youtube-comments-scraper`) Actor

For sentiment analysis, community management, creator research and AI training sets: comments and replies from any public video with text, author, channel, likes, replies and age. YouTube rounds like counts before sending them - 42 of 68 measured on 2026-09-08 - so every row says if it is exact.

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

## Pricing

from $1.46 / 1,000 comment 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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## YouTube Comments Scraper - comments, replies and likes

**For sentiment and audience research, creator and competitor analysis, moderation review, and feeding real audience language to an LLM**: comments and replies from any public YouTube video — the text, the author and their channel, the like count, the reply count, and how long ago it was posted.

**The part that makes this one different is uncomfortable to say out loud: YouTube rounds like counts before it sends them, and it never sends a real date.** `312K` is not 312,000 — it is somewhere between 311,500 and 312,499, and YouTube will not say which. A scraper that writes `"likeCount": 312000` and stops there has handed you a number that looks exact and is not. This one hands you `likeCountText: "312K"`, `likeCount: 312000` **and** `likeCountIsExact: false`, so a spreadsheet full of them can be sorted honestly.

**How often it bites, counted:** in the four pages of comments in `test/fixtures/` (8 September 2026), of **80** comments **68 carry a like number and 42 of those 68 (62%) are rounded**. Small ones (`538`, `97`, `409`) come through exact; the other **12** carry no number at all — YouTube writes those as `0 likes` in its accessibility text, and this Actor returns `0` marked exact rather than pretending the value is unknown. It depends on the video, which is exactly why the flag is per row rather than a sentence in a README: measured live the same day, a music video's top comments were around **70%** rounded while a small business channel's were near zero.

**Comments turned off is reported as comments turned off.** Measured on 8 September 2026 on six children's videos, all six of which had comments disabled — that comes back as its own row with the reason, not as a video with no comments. And paging really pages: on a video whose header claims **2,457,385** comments, **25 requests returned 500 comments with no duplicates** and a continuation to carry on from.

No API key, no login, and no proxy by default — measured 8 September 2026, a plain connection returned HTTP 200. Export as JSON, CSV or Excel.

***

### What every row carries

- **The comment**: `commentId`, `text`, `textLength`, and `commentUrl` - a link that opens that exact
  comment on YouTube
- **The author**: `authorName`, `authorChannelId`, `authorChannelUrl`, `authorIsVerified`,
  `authorIsArtist`, and `authorIsChannelOwner` (the video's own channel replying)
- **The numbers**: `likeCountText` / `likeCount` / `likeCountIsExact`, `replyCountText` / `replyCount`
- **The time**: `publishedTimeText` ("5 days ago") exactly as YouTube shows it,
  `publishedApproxDaysAgo` (5), and `isEdited` split out of the text instead of glued to it
- **The status flags**: `isPinned` with `pinnedText`, `hasCreatorHeart`, `isReply`, `replyLevel`,
  `parentCommentId`
- **The video**: `videoId`, `videoUrl`, `videoTitle`, and `videoCommentCount` - the total YouTube's own
  header claims, so you can always see how much of the comment section you actually got
- **Where it came from**: `sortedBy`, `page`, `rank`, `scrapedAt`

***

### The five things this Actor refuses to fake

**1. A rounded number is never presented as exact.** Every like count carries `likeCountIsExact`.
The `minLikes` filter says in its own description that it compares against the rounded value.

**2. The creator's heart is read from the right place.** YouTube attaches
`heartActiveTooltip: "❤ by @channel"` to **every single comment** - 90 of 90 in the fixtures. Read that
and you mark the whole comment section as hearted. The real state lives in a separate entity
(`engagementToolbarStateEntityPayload.heartState`) that has to be matched by key. In those same 90
comments, exactly **2** were actually hearted.

**3. "Comments are turned off" is never returned as "0 comments".** A video with comments disabled serves
a comment section with no comment list in it and a message saying so, and you get a free row carrying
YouTube's own wording. Measured live on
8 September 2026 on six children's videos, all six of which had comments off; one of those responses is
kept in `test/fixtures/watch-comments-off.json`. Retrying such a video will never produce rows, and this Actor says so
instead of letting you think the audience was silent.

**4. No invented dates.** YouTube gives comments relative text only. You get the text and an
*approximate* number of days, named `publishedApproxDaysAgo` because that is what it is.

**5. Coverage is stated, not implied.** YouTube serves comments about 20 at a time and stops when it
feels like it. Measured live on 8 September 2026 (this one is not in the fixtures): on a video whose header
claims 2.4 million comments, 25 requests returned **500** comments with no duplicates and a continuation
token still pending. When the rows you get are fewer than the video claims, a free row says so and why
the run stopped.

***

### Pinned comments sit first in every order

A pinned comment stays at the top whether you sort by `top` or by `newest`, so "the first row of a
newest-first run" is not the newest comment. In `test/fixtures/comments-newest.json` you can count it: row 1 is a pinned comment from a year ago, and
row 2 was posted **4 minutes** before that response was captured. Every row carries `isPinned`, so you can
drop it or keep it deliberately.

***

### Example output (one row, shortened)

```json
{
  "videoId": "dQw4w9WgXcQ",
  "videoTitle": "Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster)",
  "videoCommentCount": 2457385,
  "commentId": "Ugzge340dBgB75hWBm54AaABAg",
  "commentUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ&lc=Ugzge340dBgB75hWBm54AaABAg",
  "text": "can confirm: he never gave us up",
  "authorName": "@YouTube",
  "authorChannelId": "UCBR8-60-B28hp2BmDPdntcQ",
  "authorChannelUrl": "https://www.youtube.com/@YouTube",
  "authorIsVerified": true,
  "authorIsChannelOwner": false,
  "likeCountText": "312K",
  "likeCount": 312000,
  "likeCountIsExact": false,
  "replyCount": 963,
  "publishedTimeText": "1 year ago",
  "publishedApproxDaysAgo": 365.3,
  "isEdited": false,
  "isPinned": true,
  "pinnedText": "Pinned by @RickAstleyYT",
  "hasCreatorHeart": true,
  "isReply": false,
  "replyLevel": 0,
  "parentCommentId": null,
  "sortedBy": "top",
  "page": 1,
  "rank": 1
}
```

***

### Input

- `videoUrls` - watch URLs, `youtu.be` links, Shorts links or plain 11-character video IDs. Anything
  that is not a YouTube video comes back as a free row rather than being silently dropped.
- `maxCommentsPerVideo` - top-level comments per video (default 100).
- `maxRecords` - the most rows this run may return and charge for. When the cap bites, rows are taken
  **evenly from every video**, so no video disappears completely.
- `sortBy` - `top` (YouTube's default) or `newest`.
- `includeReplies` and `maxRepliesPerComment` - replies come back as their own rows with
  `parentCommentId` and `replyLevel: 1`.
- `keywords`, `keywordMatch`, `excludeKeywords` - matched against the comment text and author name.
  YouTube has no server-side comment search, so the filter applies to the comments this run read.
- `minLikes` - compares against the rounded like count (see above).
- `onlyVerifiedAuthors`, `postedWithinDays` - comments whose time text cannot be read are **kept**,
  not thrown away.
- `monitoringMode`, `resetMonitoringState` - remembers the comment ids already returned to you and
  returns only new ones. Run it on a schedule to watch a video and pay only for what is new.
- `maxRequests` - hard ceiling on requests to YouTube, counting video pages, comment pages and reply
  pages. It protects you from a runaway run on a video with millions of comments.
- `useProxy` - off by default. Measured 8 September 2026: a plain connection returned HTTP 200.

***

### Rows you are never charged for

Every row that is not a comment carries a reason and the words `This row is not charged`:

| `status` | when |
|---|---|
| `comments-disabled` | YouTube refuses to serve a comment section for this video |
| `invalid-input` | something you passed is not a YouTube video URL or video ID |
| `no-new-since-last-run` | monitoring mode: everything read had already been returned to you |
| `sort-unavailable` | YouTube did not offer the comment order you asked for on that video |
| `video-unavailable` | the ID is wrong, or the video is private, deleted or blocked here |
| `no-results` | the comment section loaded and was empty |
| `partial-coverage` | you got fewer comments than the video claims, and why the run stopped |
| `unreadable` | the answer could not be read - the video page is retried 3 times first |
| `bot-check` | YouTube answered with a bot check instead of comments |
| `no-match` | comments were read but none matched your filters |
| `request-limit-reached` | the run stopped at your `maxRequests` |
| `maximum-reached` | rows were left out by your `maxRecords` |
| `budget-reached` | the run hit the maximum total charge set for it |

***

### Personal data

Comment text is written by the public, and people put contact details in it. Email addresses, and runs of
10 to 13 digits that look like a phone number, are removed from the text and the author name before
anything is stored (`[email removed]`, `[phone removed]`). That covers the international and mobile forms
this Actor was tested against; it is not a guarantee that every national phone format is caught. Channel
handles and channel ids are kept - they are the public identity YouTube shows next to the comment. Links
that commenters put in their text are left as they are.

***

### How the numbers in this README can be checked

Everything above was measured, not estimated, and the responses are in `test/fixtures/` so you can count
them yourself:

```bash
npm test        # checks against those real responses, including the numbers quoted above
npm run mutate  # deliberate breakages; every one of them must turn the checks red
```

The counts in this README are themselves asserted by the test suite, so a number that drifts away from the
fixtures turns the checks red instead of quietly becoming a false claim.

# Actor input Schema

## `videoUrls` (type: `array`):

The videos whose comments you want. Paste watch URLs, youtu.be links, Shorts links or plain 11-character video IDs. Anything that is not a YouTube video is reported as its own free row instead of being silently dropped.

## `maxCommentsPerVideo` (type: `integer`):

How many top-level comments to collect for each video. YouTube serves about 20 per page and this Actor follows the pages for you. It never promises the whole comment section: measured on 8 September 2026, a video with 2.4 million comments gave 500 comments over 25 requests with a continuation token still pending.

## `maxRecords` (type: `integer`):

The most rows this run may return and charge for. When the limit cuts the result, rows are taken evenly from every video, so no video disappears completely, and a free row says what was left out.

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

top is YouTube's own default order. newest gives the most recent comments first. Note that a pinned comment stays first in both orders - every row says whether it is pinned.

## `includeReplies` (type: `boolean`):

Also fetch the replies under each comment. Replies come back as their own rows, carrying the id of the comment they answer in parentCommentId and replyLevel 1. This costs one extra request per comment that has replies.

## `maxRepliesPerComment` (type: `integer`):

How many replies to keep for each comment when replies are turned on. YouTube serves about 10 replies per request, so this Actor follows the pages until it has this many - each extra 10 costs one more request, which your Maximum requests still caps.

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

Case-insensitive words matched against the comment text and the author name. YouTube has no server-side comment search, so the filter is applied to the comments this run read - widen "Comments per video" if you are looking for something rare. Leave empty to keep everything.

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

any keeps a comment that contains at least one of the words. all requires every word.

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

Case-insensitive. A comment that contains any of these words is dropped before it is charged.

## `minLikes` (type: `integer`):

Keep only comments with at least this many likes. YouTube rounds like counts above about a thousand to "4.2K", so for those the filter uses the rounded number - each row tells you with likeCountIsExact whether its number is exact. 0 turns the filter off.

## `onlyVerifiedAuthors` (type: `boolean`):

Keep only comments written by channels YouTube marks as verified.

## `postedWithinDays` (type: `integer`):

Keep only comments posted within this many days. YouTube never returns a real date for a comment - only text like "5 days ago" - so this uses the approximate number of days derived from that text, and comments whose text cannot be read are kept rather than thrown away. 0 turns the filter off.

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

Remembers the comment ids this Actor has already returned to you and returns only the ones it has not. Run it on a schedule to watch a video and pay only for what is actually new. Set Sort by to newest when you use this: YouTube reshuffles the top order between runs, so with top you also receive old comments that simply moved into view - measured on 8 September 2026, a second run returned 5 such comments.

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

Clears that memory once, so the next monitoring run treats every comment as new.

## `maxRequests` (type: `integer`):

A hard ceiling on how many requests this run may make to YouTube, counting video pages, comment pages and reply pages. It protects you from a runaway run on a video with millions of comments.

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

Off by default: measured on 8 September 2026, a plain connection returned HTTP 200. Turn it on only if you see bot-check rows - it is slower and costs proxy traffic.

## Actor input object example

```json
{
  "videoUrls": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
  ],
  "maxCommentsPerVideo": 100,
  "maxRecords": 200,
  "sortBy": "top",
  "includeReplies": false,
  "maxRepliesPerComment": 10,
  "keywords": [],
  "keywordMatch": "any",
  "excludeKeywords": [],
  "minLikes": 0,
  "onlyVerifiedAuthors": false,
  "postedWithinDays": 0,
  "monitoringMode": false,
  "resetMonitoringState": false,
  "maxRequests": 60,
  "useProxy": false
}
```

# Actor output Schema

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

One row per comment: the comment id and a direct link to it, the text, the author with their channel id and URL, whether the author is verified or the channel owner, the like count as YouTube writes it and as a number together with a flag saying whether that number is exact, the reply count, how long ago it was posted with an approximate number of days, whether it was edited, whether it is pinned, and whether the creator hearted it. Replies carry the id of the comment they answer. Every row also carries the total number of comments YouTube claims the video has, so you can see how much of it you got. A video with comments turned off, a video that cannot be opened, a bot check, an unreadable answer, filters that matched nothing and rows left out by your maximum each come back as their own row and are not charged.

# 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 = {
    "videoUrls": [
        "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
    ],
    "maxCommentsPerVideo": 100,
    "maxRecords": 200,
    "sortBy": "top",
    "maxRepliesPerComment": 10,
    "keywords": [],
    "keywordMatch": "any",
    "excludeKeywords": [],
    "minLikes": 0,
    "postedWithinDays": 0,
    "maxRequests": 60
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/youtube-comments-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 = {
    "videoUrls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],
    "maxCommentsPerVideo": 100,
    "maxRecords": 200,
    "sortBy": "top",
    "maxRepliesPerComment": 10,
    "keywords": [],
    "keywordMatch": "any",
    "excludeKeywords": [],
    "minLikes": 0,
    "postedWithinDays": 0,
    "maxRequests": 60,
}

# Run the Actor and wait for it to finish
run = client.actor("neverempty/youtube-comments-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 '{
  "videoUrls": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
  ],
  "maxCommentsPerVideo": 100,
  "maxRecords": 200,
  "sortBy": "top",
  "maxRepliesPerComment": 10,
  "keywords": [],
  "keywordMatch": "any",
  "excludeKeywords": [],
  "minLikes": 0,
  "postedWithinDays": 0,
  "maxRequests": 60
}' |
apify call neverempty/youtube-comments-scraper --silent --output-dataset

```

## MCP server setup

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