YouTube Comments Scraper - Comment Replies & Threads
Pricing
from $0.50 / 1,000 comment or reply scrapes
YouTube Comments Scraper - Comment Replies & Threads
YouTube comments scraper that returns every comment AND every reply, with parentCommentId on every reply row so full comment threads reconstruct from one flat dataset. No login, no Google API key, no cookie.
Pricing
from $0.50 / 1,000 comment or reply scrapes
Rating
0.0
(0)
Developer
Eimantas V
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Share
YouTube Comments Scraper — Comment Replies & Threads
YouTube comments scraper that returns every comment and every reply, with
parentCommentId on every reply row so full comment threads reconstruct from
one flat dataset. No login, no Google API key, no cookie, no browser.
Most YouTube comment scrapers give you top-level comments and stop. The replies are where the argument actually happens — the creator answering a complaint, the correction that got 400 likes, the thread that turned into a support queue — and this Actor returns them as first-class rows at the same price, linked to their parent.
What you get
One row per comment and per reply. Fill rates below were measured across 325 real rows from 7 videos sampled from live search, not hand-picked:
| Field | Type | Fill | Notes |
|---|---|---|---|
commentId | string | 100% | Stable YouTube id |
parentCommentId | string | null | 100% of replies | null on top-level comments |
isReply | boolean | 100% | |
replyLevel | integer | 100% | 0 top-level, 1 reply |
text | string | 100% | Newlines preserved |
authorDisplayName | string | null | 100% | The @handle |
authorChannelId | string | null | 100% | UC… |
authorChannelUrl | string | null | 100% | |
authorAvatarUrl | string | null | 100% | |
likeCount | integer | null | 100% | Parsed from the accessibility string |
replyCount | integer | null | 100% of top-level | null on replies |
publishedTimeText | string | null | 100% | Relative only — see below |
authorIsVerified | boolean | null | 2.5% true | |
authorIsChannelOwner | boolean | null | 0.8% true | |
isHeartedByCreator | boolean | null | 14.2% true | The creator's ❤ |
isPinned | boolean | null | top-level only | Measured on 2 of 6 videos |
repliesTruncated | boolean | null | top-level only | True when the thread had more replies than you asked for |
sortBy, videoId, videoUrl, scrapedAt | 100% |
Rebuilding threads
Every reply carries its parent's id, so one group-by is enough:
const threads = new Map();for (const row of rows.filter((r) => r.commentId)) {if (row.isReply) (threads.get(row.parentCommentId) ?? []).push(row);else threads.set(row.commentId, []);}
Rows also arrive thread-adjacent — each parent is immediately followed by its own replies — so a CSV export is readable without any processing at all.
A per-video coverage row
Alongside the comments, each video gets one status row, and it is never charged:
| Field | What it tells you |
|---|---|
commentsEnabled | false when comments are turned off — a real answer, not a failure |
commentsFetched / repliesFetched | What was written |
commentsScanned | Rows examined, which is the work done |
pagesFetched | Requests spent |
totalCommentsReported | What YouTube itself claims the video has, so you can see what you missed. It is YouTube's own displayed figure, which is rounded on large videos — a video showing "10M" reports 10000000, not an exact count. Treat it as a scale check, not a precise total |
repliesTruncatedThreads | How many threads had replies you did not take |
stoppedOn | exhausted, maxCommentsPerVideo, maxTotalComments, zeroNewIds, … |
stoppedOnScanLimit | true when a work budget stopped it, not the corpus |
zeroReason | comments-disabled, no-comments-yet, or null |
This row exists because an empty dataset cannot tell you the difference between this video has no comments and we only read the first page. Those are different answers and you are entitled to know which one you got.
Input
{"videos": ["https://www.youtube.com/watch?v=jNQXAC9IVRw","dQw4w9WgXcQ","https://youtu.be/9bZkp7q19f0"],"sortBy": "top","includeReplies": true,"maxCommentsPerVideo": 100,"maxRepliesPerThread": 50,"maxTotalComments": 10000}
watch?v=, youtu.be/, /shorts/, /live/, /embed/ and bare 11-character
ids all work, and extra parameters are ignored. Shorts need no special
handling — they are ordinary videos to this endpoint. You can also chain from
another Actor's dataset with sourceDatasetId.
Comment order is top (YouTube's default) or newest. That option was verified
to actually change the output rather than merely being accepted: id-diffing Top
against Newest gave 4/10, 0/10 and 1/10 first-page overlap across three videos,
and Newest returns strict recency. Newest costs one extra request per video,
because its token only exists inside the first page's sort menu — and that
discarded page is not billed to you.
Pricing
| Event | Price | When |
|---|---|---|
comment (primary) | $0.50–$1.00 per 1,000 | Per comment or reply row |
video-processed | $0.00005 | Once per video, including comments-off |
apify-actor-start | $0.00005 | Once per run |
Replies cost the same as comments. Your whole bill is:
(videos × $0.00005) + (comment rows + reply rows) × row price + $0.00005
Never charged: status rows, error rows of any kind, retried blocked pages, and
the discarded page when using newest. Full arithmetic, including the measured
cost basis, is in docs/PRICING.md.
What this does NOT do
Stated plainly, because a listing that overpromises just collects one-star reviews:
- No absolute timestamps. YouTube returns only relative text
(
"17 hours ago"). This Actor gives you that string and does not convert it to a date, because "3 weeks ago" has a multi-day error bar and a converted value would look exact while being wrong. If you need real timestamps, the official API has them and this does not. - No transcripts, captions or subtitles. That transport was measured RED — 0 of 20 videos, including 16 that demonstrably have captions. Any Actor promising it alongside comments is promising something this endpoint no longer serves.
- No comment search or date filtering. The endpoint offers neither. Faking it by paginating everything and filtering client-side would bill you for volume you did not ask for.
- No channel or playlist expansion. Pass video URLs. A channel URL is rejected with a message telling you so, rather than guessed at.
- No sentiment scores or AI enrichment. You get the text; run your own model on it if you want that.
- No reply nesting beyond one level. YouTube itself does not have it —
replyLevelis only ever 0 or 1. - No private, deleted, or held-for-review comments. Only what a logged-out visitor can see.
- Deep reply pagination is bounded. The default takes the first 50 replies
per thread and at most 5 reply pages. When a thread has more, the parent row
carries
repliesTruncated: trueand the status row counts it — you are told, every time.
Should you use the free official API instead?
Sometimes, yes, and here is the honest test. The YouTube Data API v3 is free, gives you absolute timestamps, and has a default quota of 10,000 units/day. Comment threads cost 1 unit per call at up to 100 per page, so if you only need top-level comments, are happy to set up a Google Cloud project, and stay inside that quota, use it and do not pay for this.
This Actor earns its price when: you want replies as well as comments and do not
want to spend a second quota-unit budget on comments.list per thread; you do
not want to manage an API key or a Cloud project at all; you want the run to
tell you what it missed; or you are already orchestrating on Apify and want this
to chain onto another Actor's output.
Saying this out loud filters out the buyer who would churn — which is the point.
How it works, and the honest part
Plain HTTP JSON against YouTube's own InnerTube endpoint
(POST /youtubei/v1/next), the one youtube.com calls from your browser. No
browser is launched, which is what makes it roughly two orders of magnitude
cheaper per row than a Playwright crawler.
There is no API key, and that is measured rather than assumed: a deliberately bogus key returned HTTP 200 with 397,262 bytes of full data, and omitting the key entirely returned 459,946 bytes with a valid continuation token. So there is no credential here to expire, and nothing bound to any human account.
robots.txt
You should know this before you buy, so it is here rather than buried.
https://www.youtube.com/robots.txt contains, under User-agent: *:
Disallow: /api/Disallow: /commentDisallow: /resultsDisallow: /youtubei/
The endpoint this Actor uses, /youtubei/v1/next, is covered by that last line.
/watch is not disallowed, and is the only YouTube path this Actor touches
outside /youtubei/ — it is used solely to re-read a client version string if
one is ever retired.
This Actor accesses only publicly visible comments, sends no credentials, and bypasses no authentication or access control. It does not touch private, deleted, or moderation-held comments. Whether that is acceptable for your purpose is a decision for you and your legal advisers, and it will depend on your jurisdiction and what you do with the data. It is not a decision this README can make for you, and anyone selling you a YouTube comment scraper who does not mention robots.txt at all is not telling you less than this — they are just not telling you.
Reliability
- Rate: measured clean at 174.5 requests/minute with no delay and no rate limiting across 159 consecutive requests — the ceiling was never found. The default pacing is about a third of that, deliberately.
- Blocked pages are detected by shape, not status. A garbage input returns HTTP 200 with a normal-looking page and no error object anywhere. The Actor keys on the two top-level fields a genuine response always carries, and never on the body size — the same stub measured 55,361 bytes one day and 14,538 another.
- It fails loudly rather than quietly. A retired client version answers HTTP
404 with a ~264-byte body; the Actor rotates versions, then re-reads a live one
from
/watch, and if that also fails it errors out rather than reporting an empty success. Every video that cannot be scraped gets a typed error row in the dataset with a stableerrorClass. - Two independent budgets.
maxCommentsPerVideobounds the output;maxPagesPerVideobounds the work. Both are reported. This matters because one measured video handed back a valid continuation token on five consecutive pages while returning zero comments — the Actor terminates on zero new ids, never on a token being absent.
Proxy
Apify datacenter proxy, on by default. Residential is deliberately not offered at any price: it is billed per gigabyte, it would consume the margin this Actor is priced on, and the transport was measured not to need it.