# YouTube Comments Scraper — Replies, Likes, Authors, Dates (`scrapersdelight/youtube-comments-scraper`) Actor

Scrape every comment on any YouTube video: full text, author handle + channel ID, numeric like and reply counts, pinned / hearted-by-creator flags, approximate date and the comment permalink. Threaded replies included. Sort by top or newest. No API key, no quota, no login.

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

## Pricing

$0.25 / 1,000 per 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.

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

**Export every comment on any YouTube video — text, author, likes, replies, dates — as CSV, JSON or Excel. No API key, no quota, no login.**

The official YouTube Data API gives you 10,000 quota units a day, which is about **100 videos' worth of comments** before it shuts you off for 24 hours. This Actor doesn't use it. It reads the same public data the video page itself loads, so there is no quota, no OAuth screen and no Google Cloud project to set up.

You paste video URLs. You get a clean, flat table — one row per comment, one row per reply, with the video's own context (title, channel, view count, total comment count) carried on every row so a spreadsheet is instantly pivotable.

***

### ⚡ What does YouTube Comments Scraper do?

- 💬 **Every comment**, paged as deep as you want — 20 per request, no ceiling from YouTube's side
- 🧵 **Threaded replies** as their own rows, linked to their parent by `parentCommentId`
- 👤 **Real author identity** — `@handle`, channel ID *and* a working channel URL, avatar, verified badge
- ❤️ **Engagement that's actually a number** — `likeCount: 310000`, not the string `"310K"`
- 📌 **Pinned + hearted-by-creator flags** — the two signals that tell you what the channel owner endorsed
- 🔗 **A permalink per comment** (`…&lc=<commentId>`) that opens that exact comment in a browser
- 🗓️ **`publishedAtApprox`** — YouTube's relative age resolved to an ISO timestamp you can sort and filter
- 🔀 **Top or Newest** ordering, using YouTube's own sort tokens — and `position` preserves that ranking
- ⏱️ **`newerThanDays`** — stop paging the moment the feed goes stale (the cheap way to monitor a video)
- 🎬 Accepts **watch URLs, youtu.be links, /shorts/, /live/ and bare video IDs**

***

### 📊 Output — 39 fields per row

Measured on **50 real records** (two comment pages + one reply page from `dQw4w9WgXcQ`, captured 2026‑09‑02). The fill percentages below are **counted, not estimated**. The five conditional fields (`replyCountText`, `pinnedByText`, `parentCommentId`, and the three video fields at 44%) are conditional *by definition* — a reply has no reply count, an unpinned comment has no pin label — so their share moves with what you ask for. Everything marked 100% was 100% here and again on the 150-row platform run below.

| Field | Fill | What it is |
|---|---|---|
| `commentId` | 100% | YouTube's own comment ID |
| `commentUrl` | 100% | Permalink that opens this comment on the video |
| `text` | 100% | The full comment body |
| `publishedTimeText` | 100% | YouTube's own wording — `"3 days ago"`, `"6 years ago (edited)"` |
| `publishedAtApprox` | 100% | That age resolved to an ISO timestamp (see the caveat below) |
| `isEdited` | 100% | True when YouTube marks the comment `(edited)` |
| `likeCount` | 100% | **Numeric.** `"310K"` → `310000` |
| `likeCountText` | 100% | YouTube's abbreviated original, kept for auditing |
| `replyCount` | 100% | Numeric reply count on the thread |
| `replyCountText` | 74% | Original text; absent on replies and on threads with zero replies |
| `isPinned` | 100% | Pinned by the channel |
| `pinnedByText` | 2% | `"Pinned by @RickAstleyYT"` — only on the pinned comment |
| `isHeartedByCreator` | 100% | The creator's ❤ on the comment |
| `isReply` / `replyLevel` | 100% | Top-level (0) vs reply (1) |
| `parentCommentId` | 20% | Set on reply rows — joins a reply back to its parent |
| `authorName` | 100% | `@handle` |
| `authorChannelId` / `authorChannelUrl` | 100% | The commenter's channel, and a URL that opens it |
| `authorAvatarUrl` | 100% | Profile image |
| `authorIsVerified` / `authorIsChannelOwner` / `authorIsArtist` | 100% | Badges |
| `position` | 100% | 1..n in the order YouTube served them, so `sortBy: top` stays ranked |
| `videoId` / `videoUrl` | 100% | The source video |
| `videoTitle` / `videoChannelName` / `videoChannelId` / `videoChannelUrl` | 100% | Video + uploader context on every row |
| `videoViewCount` / `videoPublishedAt` | 100% | Numeric views, ISO publish date |
| `videoLengthSeconds` / `videoCategory` / `videoIsLive` | 44% | Present unless YouTube serves the stripped watch page (see Limits) |
| `videoTotalComments` / `videoTotalCommentsText` | 100% | The video's whole comment count, e.g. `2456556` |
| `sortedBy` / `scrapedAt` | 100% | Which ordering produced the row, and when |

#### Sample row

```json
{
  "commentId": "UgwxEqSh_78DU_MOKpt4AaABAg",
  "commentUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ&lc=UgwxEqSh_78DU_MOKpt4AaABAg",
  "text": "Never thought the video was taken down, not even for a second. I just wanted to hear my boy Rick Astley again.",
  "publishedTimeText": "1 month ago",
  "publishedAtApprox": "2026-08-02T17:47:56.261Z",
  "isEdited": false,
  "likeCount": 6700,
  "likeCountText": "6.7K",
  "replyCount": 46,
  "isPinned": false,
  "isHeartedByCreator": false,
  "isReply": false,
  "replyLevel": 0,
  "parentCommentId": null,
  "authorName": "@Hushoo",
  "authorChannelId": "UCTfgDEMJhUnGz24yinDMenQ",
  "authorChannelUrl": "https://www.youtube.com/channel/UCTfgDEMJhUnGz24yinDMenQ",
  "authorIsVerified": false,
  "position": 2,
  "videoId": "dQw4w9WgXcQ",
  "videoTitle": "Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster)",
  "videoChannelName": "Rick Astley",
  "videoViewCount": 1810785988,
  "videoTotalComments": 2456556,
  "sortedBy": "top",
  "scrapedAt": "2026-09-02T04:17:56.261Z"
}
```

***

### 💵 Pricing

**$0.00025 per comment** — pay-per-event, one event, nothing else. No monthly rental, no charge for starting a run, no separate fee for replies.

| You scrape | You pay |
|---|---|
| 100 comments | **$0.025** |
| 1,000 comments | **$0.25** |
| 10,000 comments | **$2.50** |
| 100,000 comments | **$25.00** |

Billing is **budget-aware**: rows are delivered and charged in the same atomic call, so if you set a spend cap the run stops at it. You are never billed for a row you didn't receive, and you never receive rows you weren't billed for.

Two caps keep a run inside the budget you meant: **`maxComments`** (per video) and **`maxItems`** (whole run). Both are on by default — 100 and 1,000.

***

### 🚀 How to use it

1. Paste one or more video URLs into **📺 YouTube videos**.
2. Pick **Top** (YouTube's ranking) or **Newest** (chronological).
3. Set **Max comments per video** and, if you want the threads, switch on **Include threaded replies**.
4. Run. Export as CSV, JSON, Excel, or pull it from the API.

#### Input example

```json
{
  "videoUrls": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "https://youtu.be/9bZkp7q19f0"
  ],
  "sortBy": "newest",
  "includeReplies": true,
  "maxRepliesPerComment": 3,
  "maxComments": 25,
  "maxItems": 45
}
```

Monitoring a video for new comments? Use `sortBy: "newest"` with `newerThanDays: 7` — the run stops paging the moment the feed goes older than a week, so a daily check costs almost nothing.

***

### 🧪 What has actually been verified

Everything below was measured on **2026‑09‑02**, not assumed.

- **The data path** — watch page → InnerTube `/next` → continuation. All three hops returned **HTTP 200 on the first try**, from a home IP (1,390,091 / 513,200 / 264,297 bytes) **and** through an Apify **datacenter** proxy (1,303,721 / 431,832 / 263,605 bytes). Residential proxies are not required.
- **Pagination** — page 1 and page 2 each returned 20 comments with **0 overlapping comment IDs**.
- **Replies** — a reply thread returned 10 replies, all with `replyLevel: 1` and all linked to the right parent.
- **The parser** — **76 assertions** run offline against the captured bytes: field values, counts, ordering, ID uniqueness, like-count conversion, date resolution, URL parsing. All pass.
- **On the Apify platform, not just locally** — a 3-video run with replies on returned **150 rows in 15.9 seconds**, 3 of 3 videos, **150 unique comment IDs**, 39 fields on every row, exit code 0.
- **Billing integrity** — a 200-row run reported `chargedEventCounts: {"comment-scraped": 200}` against a dataset holding exactly **200 items**. Delivered equals billed, to the row.
- **Caps are exact** — a run capped at 25 per video and 45 for the whole run returned exactly 25, 20 and 45. Peak memory on a 200-row run was **73 MB** against the 1 GB the Actor asks for.

***

### ⚠️ Honest limits

- **There is no exact comment timestamp on YouTube.** Anywhere. YouTube publishes only a relative age (`"3 days ago"`). `publishedTimeText` is that string verbatim; `publishedAtApprox` is it resolved against the scrape time, so it is precise to YouTube's own granularity — hours, days, weeks, months, years — and no better. It is called *Approx* on purpose. Any scraper claiming an exact posting time for a YouTube comment is computing the same estimate and not telling you.
- **Like counts are abbreviated at the source.** YouTube renders `"310K"`, so `likeCount: 310000` is exact to three significant figures, not to the unit. `likeCountText` keeps the original so you can see what was rounded.
- **Some videos serve a stripped watch page.** When YouTube decides an exit IP needs a sign-in to *play* a video (`playabilityStatus: LOGIN_REQUIRED`), it removes the player metadata block. The comments still come back in full — this was measured returning 25 comments under exactly that status — and title, channel, views and publish date are recovered from the page's other data block. `videoLengthSeconds`, `videoCategory` and `videoIsLive` only live in the removed block, so those three come back `null` for those videos. They are the 44% column in the table above.
- **Comments turned off** is reported as a per-video note in the run status, never as an empty result. Same for a members-only or age-gated video.
- **A refused exit IP is never reported as "no comments."** Refused requests are counted by status code and named in the run's status message. If YouTube starts refusing your IPs on a very large job, lower **Videos in parallel** or switch the proxy to residential.
- **Sorting by Newest includes spam.** That is YouTube's own warning on the sort menu, not ours — `newest` is unfiltered chronological order, `top` is YouTube's curated ranking.

***

### ❓ FAQ

**Do I need a YouTube API key or a Google account?**
No. No key, no OAuth, no Google Cloud project. This reads the same public data your browser loads on the video page.

**How is this different from the official YouTube Data API?**
The official API costs quota — 10,000 units/day, roughly 100 videos of comments, then you wait 24 hours. This has no quota and no daily reset. It also returns fields the API doesn't expose in one call, such as the creator's ❤, the pinned flag, and the author's channel URL.

**Can I get replies, not just top-level comments?**
Yes — switch on **Include threaded replies**. Each reply is its own row with `isReply: true`, `replyLevel: 1` and `parentCommentId` pointing at the comment it answers. Cap them per thread with **Max replies per comment**.

**Can I scrape many videos in one run?**
Yes. Paste as many as you like; they're worked on in parallel (default 3 at a time). `maxItems` caps the whole run so a long list can't surprise your bill.

**Does it work on YouTube Shorts?**
Yes. `/shorts/` URLs are accepted, and Shorts comments come back through the same path.

**What about live streams?**
Comments on a finished live stream work like any other video. Live *chat* during a broadcast is a different system and is not covered here.

**Can I only get recent comments?**
Set `sortBy: "newest"` and `newerThanDays`. The run stops paging as soon as the feed goes older than your cutoff, so you pay for the new comments and nothing else.

**How many comments can I get from one video?**
As many as the video has. Set `maxComments: 0` to lift the per-video cap (`maxItems` still guards the run). Deep pulls take longer — every 20 comments is one request — so raise the run timeout for six-figure jobs.

**Are comments returned in YouTube's order?**
Yes. Items are read in the order YouTube serves them and `position` records it, so `sortBy: "top"` gives you a genuine ranking rather than a shuffled bag.

**Why do some rows have `null` for video length or category?**
See Honest limits — those three fields live in a block YouTube strips from the watch page on some exit IPs. Everything else, including title, channel and view count, is recovered from elsewhere on the page.

**Do I need residential proxies?**
No. The whole chain was verified returning 200 through Apify's standard datacenter proxies. Residential is there if you push very large volumes.

**Which export formats are supported?**
CSV, JSON, JSONL, Excel, XML and HTML table, plus the Apify API and integrations (Make, Zapier, Google Sheets, webhooks).

**Can I schedule it?**
Yes — Apify Schedules. A daily `sortBy: newest` + `newerThanDays: 1` run is a cheap brand-mention monitor.

***

### ⚖️ Legal & fair use

This Actor reads **publicly visible** YouTube comments — the same data any signed-out visitor sees on the video page. It does not log in, does not bypass a paywall or an age gate, and collects no private or hidden content.

Comment text and author handles are **personal data** in the EU/UK. If you process them you are the data controller: have a lawful basis under GDPR (or your local equivalent), keep only what you need, and honour deletion requests. Respect YouTube's Terms of Service and use the output responsibly — public-opinion research, brand and sentiment monitoring, creator analytics, moderation review.

You are responsible for how you use what you collect.

# Actor input Schema

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

One video per line. Accepts a normal watch URL, a youtu.be short link, a /shorts/ or /live/ URL, or a bare 11-character video ID. Anything that cannot be parsed is skipped and named in the log.

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

`top` is YouTube's own featured/most-engaged ordering (the default a viewer sees). `newest` is strictly chronological, newest first — use it for monitoring and with 'Only comments newer than'.

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

Also expand each comment's reply thread. Replies arrive as their own rows, directly after their parent, with `isReply: true` and `parentCommentId` set. Off by default because replies cost one extra request per thread and count towards your limits.

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

Cap on replies pulled for a single comment thread when replies are switched on. 0 = every reply (hard-capped at 500 per thread).

## `maxComments` (type: `integer`):

Stop after this many rows for each video. Replies count towards it when they are switched on. 0 = no per-video limit (the run-wide 'Max rows' below still applies).

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

Hard stop across every video, so a long video list can never bill more than you planned. 0 = no run-wide limit.

## `newerThanDays` (type: `integer`):

Skip comments older than this many days. 0 = no cutoff. YouTube only publishes a relative age ('3 days ago'), so the cutoff is exact to that granularity and no better. With sort = newest it also stops paging as soon as the feed goes older, which is the cheap way to monitor a video.

## `videoConcurrency` (type: `integer`):

How many videos to work on at once. Comments inside one video must be paged in order, so this is where the speed comes from on a long list. Lower it if you see refused requests.

## `requestDelayMs` (type: `integer`):

Optional pause between page requests. 0 is fine for normal use; raise it only if YouTube starts refusing your exit IPs on a very large run.

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

Apify Proxy is enough — the whole 3-request InnerTube chain was verified returning HTTP 200 through standard datacenter proxies. Switch to residential only if you are running very large jobs and start seeing refused requests in the log.

## Actor input object example

```json
{
  "videoUrls": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
  ],
  "sortBy": "top",
  "includeReplies": false,
  "maxRepliesPerComment": 10,
  "maxComments": 30,
  "maxItems": 30,
  "newerThanDays": 0,
  "videoConcurrency": 3,
  "requestDelayMs": 0,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `comments` (type: `string`):

The dataset of scraped YouTube comments (one item per comment or reply).

# 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"
    ],
    "maxComments": 30,
    "maxItems": 30
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/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"],
    "maxComments": 30,
    "maxItems": 30,
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/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"
  ],
  "maxComments": 30,
  "maxItems": 30
}' |
apify call scrapersdelight/youtube-comments-scraper --silent --output-dataset

```

## MCP server setup

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