YouTube Video Comment & Reply Thread Scraper
Get a YouTube video's comments with nested replies attached, author identity, like counts, and pinned/hearted/verified flags, filterable by keyword and minimum likes.
Input schema
Input fields, types, required flags, descriptions, defaults, and prefills come from the published definition. Prefill examples are distinct from defaults.
| Field | Type | Required | Description |
|---|---|---|---|
videoUrls | array | optional | Watch, youtu.be, Shorts, embed, live, v/ links, or bare 11-character IDs. Every video is normalized to a watch URL internally. |
channelUrls | array | optional | Channel /videos sources: /@handle, /channel/UC..., /c/name, /user/name, a bare @handle, or a UC... channel ID. Scrapes up to Max videos per source. |
playlistUrls | array | optional | Playlist sources: youtube.com/playlist?list=... or a bare playlist ID. Scrapes up to Max videos per source. |
Show all input fields
| Field | Type | Required | Description |
|---|---|---|---|
videoUrls | array | optional | Watch, youtu.be, Shorts, embed, live, v/ links, or bare 11-character IDs. Every video is normalized to a watch URL internally. |
channelUrls | array | optional | Channel /videos sources: /@handle, /channel/UC..., /c/name, /user/name, a bare @handle, or a UC... channel ID. Scrapes up to Max videos per source. |
playlistUrls | array | optional | Playlist sources: youtube.com/playlist?list=... or a bare playlist ID. Scrapes up to Max videos per source. |
searchQueries | array | optional | YouTube search terms or search-result URLs. Only video results are used, deduplicated, up to Max videos per source. |
maxVideosPerSource | integer | optional | Discovery cap for channel, playlist and search sources. 0 = every video the source exposes. |
maxComments | integer | optional | Top-level comments to examine per video. 0 = all. Comments rejected by filters still count toward this scan cap (so restrictive filters cannot paginate forever); replies do not. Both emitted comments and replies count toward Max total rows. |
includeReplies | boolean | optional | Fetch reply threads. Emitted as flat rows by default; use Output mode tree to embed them instead. |
maxRepliesPerComment | integer | optional | Per-thread reply cap. 0 = all replies, stopping when the continuation token is exhausted. YouTube serves roughly 9-10 on the first reply page and 44-49 afterwards. |
maxReplyDepth | integer | optional | 0 = top-level comments only, 1 = direct replies (the shape observed publicly). Values above 1 are reserved for nested sub-threads, which YouTube has not exposed in the samples used to build this actor. |
maxTotalRows | integer | optional | Global budget for emitted comment + reply rows. 0 = unlimited. Status rows are never counted and are not billable data rows. |
sortBy | string | optional | top = YouTube relevance order; newest = chronological, recommended for date cutoffs and complete windows. A date cutoff forces newest automatically. |
commentsSince | string | optional | Optional cutoff as an ISO date/datetime (2026-10-16) or relative duration (7d, 24h, 30m). Forces newest sort. Pinned comments are always kept; pagination stops at the first non-pinned comment older than the cutoff. |
keywords | array | optional | Case-insensitive OR filter on comment and reply text. Empty = no keyword filter. A parent comment that does not match is skipped, so its replies are not fetched. |
minLikes | integer | optional | Drop comments and replies below this like count. Applied after fetching. YouTube only exposes rounded counts (K/M), so the comparison uses the rounded value. |
onlyCreatorHearted | boolean | optional | Keep only comments/replies that the video creator hearted. HeartedBy identifies the creator where YouTube exposes the tooltip. |
outputMode | string | optional | flat = one row per comment and reply (recommended for CSV/Excel/SQL); tree = parent rows with embedded replies[] (backward compatible); both = flat rows plus parent rows carrying replies[]. |
emitStatusRows | boolean | optional | Write one recordType=status row for blocked, private, unavailable, comments-off, empty-token, invalid-input, partial and budget-stop cases. Status rows are never counted toward Max total rows; disable this to keep the dataset data-only. |
Download the published input schema
View complete JSON
{
"description": "Scrape public YouTube comments and reply threads without login, user cookies, or an API key. Provide one or more videos, channels, playlists, or search queries.",
"properties": {
"channelUrls": {
"default": [],
"description": "Channel /videos sources: /@handle, /channel/UC..., /c/name, /user/name, a bare @handle, or a UC... channel ID. Scrapes up to Max videos per source.",
"editor": "stringList",
"items": {
"type": "string"
},
"prefill": [],
"title": "Channel URLs or handles",
"type": "array"
},
"commentsSince": {
"description": "Optional cutoff as an ISO date/datetime (2026-10-16) or relative duration (7d, 24h, 30m). Forces newest sort. Pinned comments are always kept; pagination stops at the first non-pinned comment older than the cutoff.",
"editor": "textfield",
"title": "Comments since",
"type": "string"
},
"emitStatusRows": {
"default": true,
"description": "Write one recordType=status row for blocked, private, unavailable, comments-off, empty-token, invalid-input, partial and budget-stop cases. Status rows are never counted toward Max total rows; disable this to keep the dataset data-only.",
"prefill": true,
"title": "Emit status rows",
"type": "boolean"
},
"includeReplies": {
"default": true,
"description": "Fetch reply threads. Emitted as flat rows by default; use Output mode tree to embed them instead.",
"prefill": true,
"title": "Include replies",
"type": "boolean"
},
"keywords": {
"default": [],
"description": "Case-insensitive OR filter on comment and reply text. Empty = no keyword filter. A parent comment that does not match is skipped, so its replies are not fetched.",
"editor": "stringList",
"items": {
"type": "string"
},
"prefill": [],
"title": "Keywords (ANY match)",
"type": "array"
},
"maxComments": {
"default": 100,
"description": "Top-level comments to examine per video. 0 = all. Comments rejected by filters still count toward this scan cap (so restrictive filters cannot paginate forever); replies do not. Both emitted comments and replies count toward Max total rows.",
"editor": "number",
"maximum": 50000,
"minimum": 0,
"prefill": 3,
"title": "Max top-level comments per video",
"type": "integer"
},
"maxRepliesPerComment": {
"default": 50,
"description": "Per-thread reply cap. 0 = all replies, stopping when the continuation token is exhausted. YouTube serves roughly 9-10 on the first reply page and 44-49 afterwards.",
"editor": "number",
"maximum": 10000,
"minimum": 0,
"prefill": 3,
"title": "Max replies per comment",
"type": "integer"
},
"maxReplyDepth": {
"default": 1,
"description": "0 = top-level comments only, 1 = direct replies (the shape observed publicly). Values above 1 are reserved for nested sub-threads, which YouTube has not exposed in the samples used to build this actor.",
"editor": "number",
"maximum": 6,
"minimum": 0,
"prefill": 1,
"title": "Max reply depth",
"type": "integer"
},
"maxTotalRows": {
"default": 5000,
"description": "Global budget for emitted comment + reply rows. 0 = unlimited. Status rows are never counted and are not billable data rows.",
"editor": "number",
"maximum": 1000000,
"minimum": 0,
"prefill": 20,
"title": "Max total rows",
"type": "integer"
},
"maxVideosPerSource": {
"default": 20,
"description": "Discovery cap for channel, playlist and search sources. 0 = every video the source exposes.",
"editor": "number",
"maximum": 5000,
"minimum": 0,
"title": "Max videos per channel/playlist/search",
"type": "integer"
},
"minLikes": {
"default": 0,
"description": "Drop comments and replies below this like count. Applied after fetching. YouTube only exposes rounded counts (K/M), so the comparison uses the rounded value.",
"editor": "number",
"maximum": 1000000000,
"minimum": 0,
"prefill": 0,
"title": "Minimum likes",
"type": "integer"
},
"onlyCreatorHearted": {
"default": false,
"description": "Keep only comments/replies that the video creator hearted. HeartedBy identifies the creator where YouTube exposes the tooltip.",
"prefill": false,
"title": "Only creator-hearted comments",
"type": "boolean"
},
"outputMode": {
"default": "flat",
"description": "flat = one row per comment and reply (recommended for CSV/Excel/SQL); tree = parent rows with embedded replies[] (backward compatible); both = flat rows plus parent rows carrying replies[].",
"editor": "select",
"enum": [
"flat",
"tree",
"both"
],
"enumTitles": [
"Flat rows",
"Tree with embedded replies",
"Both"
],
"prefill": "flat",
"title": "Output mode",
"type": "string"
},
"playlistUrls": {
"default": [],
"description": "Playlist sources: youtube.com/playlist?list=... or a bare playlist ID. Scrapes up to Max videos per source.",
"editor": "stringList",
"items": {
"type": "string"
},
"prefill": [],
"title": "Playlist URLs or IDs",
"type": "array"
},
"searchQueries": {
"default": [],
"description": "YouTube search terms or search-result URLs. Only video results are used, deduplicated, up to Max videos per source.",
"editor": "stringList",
"items": {
"type": "string"
},
"prefill": [],
"title": "Search queries",
"type": "array"
},
"sortBy": {
"default": "top",
"description": "top = YouTube relevance order; newest = chronological, recommended for date cutoffs and complete windows. A date cutoff forces newest automatically.",
"editor": "select",
"enum": [
"top",
"newest"
],
"enumTitles": [
"Top comments",
"Newest first"
],
"prefill": "top",
"title": "Sort comments by",
"type": "string"
},
"videoUrls": {
"default": [],
"description": "Watch, youtu.be, Shorts, embed, live, v/ links, or bare 11-character IDs. Every video is normalized to a watch URL internally.",
"editor": "stringList",
"items": {
"type": "string"
},
"prefill": [
"https://www.youtube.com/watch?v=dQw4w9WgXcQ"
],
"title": "Video URLs or IDs",
"type": "array"
}
},
"schemaVersion": 1,
"title": "YouTube Comments & Reply Thread Scraper Input",
"type": "object"
}
Output schema
Published result links and storage locations. This schema is not an exhaustive contract for every field in a dataset record.
| Field | Type | Required | Description |
|---|---|---|---|
comments | string | optional | Dataset containing one row per comment or reply (plus optional status rows when emitStatusRows is enabled). |
Download the published output schema
View complete JSON
{
"actorOutputSchemaVersion": 1,
"description": "Flat comment and reply rows with author identity, rounded engagement counts, honest approximate dates, thread fields, video statistics, and optional status rows.",
"properties": {
"comments": {
"description": "Dataset containing one row per comment or reply (plus optional status rows when emitStatusRows is enabled).",
"template": "{{links.apiDefaultDatasetUrl}}/items",
"title": "YouTube comments, replies and statuses",
"type": "string"
}
},
"title": "YouTube Video Comment & Reply Thread Scraper Output"
}
Dataset schema
Published dataset views and any declared field definitions. A view can select only a subset of the returned record fields.
Download the published dataset schema
View complete JSON
{
"actorSpecification": 1,
"views": {
"overview": {
"display": {
"component": "table",
"properties": {
"authorName": {
"format": "text",
"label": "Author"
},
"commentId": {
"format": "text",
"label": "Comment ID"
},
"isHearted": {
"format": "boolean",
"label": "Hearted"
},
"likeCount": {
"format": "number",
"label": "Likes"
},
"recordType": {
"format": "text",
"label": "Type"
},
"replyCount": {
"format": "number",
"label": "Replies"
},
"text": {
"format": "text",
"label": "Text"
},
"videoTitle": {
"format": "text",
"label": "Video"
}
}
},
"title": "Comments and replies",
"transformation": {
"fields": [
"recordType",
"commentId",
"authorName",
"text",
"likeCount",
"replyCount",
"isHearted",
"videoTitle"
]
}
},
"thread": {
"display": {
"component": "table",
"properties": {
"authorHandle": {
"format": "text",
"label": "Handle"
},
"commentId": {
"format": "text",
"label": "Comment ID"
},
"creatorReplied": {
"format": "boolean",
"label": "Creator replied"
},
"heartedBy": {
"format": "text",
"label": "Hearted by"
},
"isPinned": {
"format": "boolean",
"label": "Pinned"
},
"likeCountText": {
"format": "text",
"label": "Likes (text)"
},
"parentCommentId": {
"format": "text",
"label": "Parent ID"
},
"publishedAtApprox": {
"format": "text",
"label": "Approx. published"
},
"publishedAtPrecision": {
"format": "text",
"label": "Date precision"
},
"publishedTimeText": {
"format": "text",
"label": "Published"
},
"recordType": {
"format": "text",
"label": "Type"
},
"replyCountText": {
"format": "text",
"label": "Replies (text)"
},
"replyLevel": {
"format": "number",
"label": "Level"
},
"rootCommentId": {
"format": "text",
"label": "Root ID"
},
"sourceInput": {
"format": "text",
"label": "Source input"
},
"sourceScope": {
"format": "text",
"label": "Source scope"
},
"threadPosition": {
"format": "number",
"label": "Position"
},
"videoId": {
"format": "text",
"label": "Video ID"
}
}
},
"title": "Thread fields",
"transformation": {
"fields": [
"recordType",
"commentId",
"parentCommentId",
"rootCommentId",
"replyLevel",
"threadPosition",
"authorHandle",
"likeCountText",
"replyCountText",
"isPinned",
"heartedBy",
"creatorReplied",
"publishedTimeText",
"publishedAtApprox",
"publishedAtPrecision",
"videoId",
"sourceInput",
"sourceScope"
]
}
}
}
}
Key-value schema
Published key-value store definitions, when provided by this build.
This definition is not present in the default published build. See the published README for available documentation.
Published README
Read the full published documentation
YouTube Video Comment & Reply Thread Scraper
Scrape YouTube comments and reply threads as analysis-ready rows. Give it video links, channel video tabs, playlists, or search queries, and get one row per comment or reply with author identity, engagement signals, thread position, and video context. Works on public videos without user cookies, login, or an API key.
What this actor does
- Four source types: individual videos (watch, Shorts, live, embed, youtu.be, bare ID), channel video tabs, playlists, and video search results
- Flat rows by default: every comment and reply is its own row with
parentCommentId,rootCommentId,replyLevel, andthreadPosition, so CSV, Excel, SQL, and sentiment pipelines can use it directly - Full reply threads: fetch every page of a thread, cap per comment, or skip replies entirely;
maxRepliesPerComment: 0means all - Real filters: Top/Newest sort, date cutoff, multiple keywords (ANY match), minimum likes, and only creator-hearted comments
- Honest engagement data: rounded like counts with the original text, advertised reply counts, pinned/hearted/verified flags, and approximate dates with a precision and range
- Video context on every row: title, channel, views, likes, comment count, duration, publish date, and description
- Empty fields are omitted — records only contain values that are actually populated.
Output per comment or reply
Each dataset row is one comment (recordType: "comment", replyLevel: 0) or one reply (recordType: "reply", replyLevel: 1).
| Field | Type | Description |
|---|---|---|
recordType |
string | comment, reply, or status |
commentId |
string | YouTube comment identifier |
parentCommentId |
string | Root comment ID on replies; omitted on top-level comments |
rootCommentId |
string | Thread root ID (own ID on top-level comments) |
replyLevel |
integer | 0 for comments, 1 for direct replies |
threadPosition |
integer | 1-based order in the emitted feed or reply thread |
isReply |
boolean | Backward-compatible alias for replyLevel > 0 |
replyDepth |
integer | Backward-compatible alias for replyLevel |
text |
string | Comment text, with unicode and newlines preserved |
authorName |
string | Display name |
authorHandle |
string | @handle when YouTube exposes it |
authorChannelId |
string | UC... channel ID |
authorChannelUrl |
string | https://www.youtube.com/@handle or /channel/UC... |
authorAvatarUrl |
string | Avatar image URL |
authorIsVerified |
boolean | Author channel is verified |
authorIsCreator |
boolean | Author is the video's channel owner |
authorIsArtist |
boolean | Author is marked as a YouTube Music artist |
likeCount |
integer | Rounded like count (69K becomes 69000) |
likeCountText |
string | Original formatted count (69K) |
replyCount |
integer | YouTube's advertised reply count; may exceed visible replies |
replyCountText |
string | Original reply-count text when YouTube exposes it (963 replies) |
repliesFetched |
integer | Reply rows actually attached/emitted for this comment (present when replies were fetched) |
isPinned |
boolean | Comment is pinned |
pinnedByText |
string | Pinned-by tooltip (Pinned by @channel) |
isHearted |
boolean | Creator hearted the comment |
heartedBy |
string | Creator handle from the heart tooltip |
heartedByText |
string | Normalized creator label for the heart |
creatorReplied |
boolean | Whether any emitted reply is authored by the video creator (present when replies were fetched) |
creatorReplyCount |
integer | Number of emitted replies authored by the creator (present when replies were fetched) |
isEdited |
boolean | Comment was edited, derived from the (edited) label |
publishedTimeText |
string | Relative publish text exactly as YouTube shows it |
publishedAtApprox |
string | Midpoint of the inferred date range (ISO-8601) |
publishedAtFrom |
string | Earliest possible publish time |
publishedAtTo |
string | Latest possible publish time |
publishedAtPrecision |
string | second, minute, hour, day, week, month, or year |
ageDaysMin |
number | Lower bound of comment age in days |
ageDaysMax |
number | Upper bound of comment age in days |
ageDaysApprox |
number | Midpoint of the age range |
commentUrl |
string | Direct link to the comment (...&lc=COMMENT_ID) |
videoId |
string | Video ID |
videoUrl |
string | Canonical watch URL |
videoTitle |
string | Video title |
videoChannelId |
string | Video channel ID |
videoChannelHandle |
string | Video channel path (/@handle) |
videoChannelName |
string | Video channel display name |
videoViewCount |
integer | View count |
videoLikeCount |
integer | Video like count (YouTube may round it) |
videoCommentCount |
integer | Approximate total comments shown by YouTube |
videoPublishDate |
string | Raw publish date text (Oct 24, 2009) |
videoPublishedAt |
string | Parsed publish date (2009-10-24) |
videoDurationSeconds |
integer | Video length in seconds |
videoDescription |
string | Video description |
sourceInput |
string | The original URL, handle, or query that produced this video |
sourceScope |
string | video, channel, playlist, or search |
sourceVideoId |
string | Video selected from the source |
scrapedAt |
string | ISO-8601 UTC timestamp of the run |
replies |
array | Embedded reply rows, only with outputMode: tree or both |
Status rows
When emitStatusRows is enabled, blocked/empty/unavailable sources and partial or budget-limited runs add one row with recordType: "status", status, inputUrl, optional video fields, error, and rowsEmitted when known. Status rows are never counted toward maxTotalRows and do not consume the data-row budget. Status values: no_comments, comments_off, unavailable, private, age_restricted, members_only, region_blocked, live_chat_only, blocked, stale_token, transient_empty, parse_empty, partial, budget_reached, input_invalid. Successful comment runs do not add an ok status row.
Dates and counts are approximate
YouTube's public comment payload does not expose exact timestamps. Every date field is derived from the relative label (6 years ago) and comes with a precision plus earliest/latest bounds, so no exact timestamp is invented. Like counts above 999 are rounded by YouTube itself; likeCountText preserves the original value. replyCount is YouTube's advertised count and can be higher than the number of replies the public thread returns.
Input
| Field | Type | Default | Description |
|---|---|---|---|
videoUrls |
array | [] |
Video URLs or bare IDs (watch, youtu.be, Shorts, embed, live, v/) |
channelUrls |
array | [] |
Channel video tabs: /@handle, /channel/UC..., /c/name, /user/name, @handle, or UC... |
playlistUrls |
array | [] |
Playlist URLs or bare playlist IDs |
searchQueries |
array | [] |
Search terms or YouTube search-result URLs; video results only |
maxVideosPerSource |
integer | 20 |
Video discovery cap per channel/playlist/search; 0 = all |
maxComments |
integer | 100 |
Top-level comments to examine per video; 0 = all. Filtered-out comments still count toward this scan cap |
includeReplies |
boolean | true |
Fetch reply threads |
maxRepliesPerComment |
integer | 50 |
Replies per thread; 0 = all until the token is exhausted |
maxReplyDepth |
integer | 1 |
0 = top-level only, 1 = direct replies |
maxTotalRows |
integer | 5000 |
Global comment + reply budget; 0 = unlimited |
sortBy |
string | top |
top or newest |
commentsSince |
string | – | ISO date/datetime or relative duration (7d, 24h); forces newest |
keywords |
array | [] |
Case-insensitive ANY-match filter on comment/reply text |
minLikes |
integer | 0 |
Drop rows below this rounded like count |
onlyCreatorHearted |
boolean | false |
Keep only creator-hearted rows |
outputMode |
string | flat |
flat, tree, or both |
emitStatusRows |
boolean | true |
Emit recordType: "status" rows for blocked/empty/invalid sources |
At least one of videoUrls, channelUrls, playlistUrls, or searchQueries is required.
Example: one video with replies
{
"videoUrls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],
"maxComments": 50,
"includeReplies": true,
"maxRepliesPerComment": 20,
"sortBy": "top",
"maxTotalRows": 500
}
Example: recent comments from a channel
{
"channelUrls": ["https://www.youtube.com/@RickAstleyYT"],
"maxVideosPerSource": 10,
"sortBy": "newest",
"commentsSince": "7d",
"maxComments": 100,
"minLikes": 5,
"maxTotalRows": 1000
}
Example: search with creator-hearted comments
{
"searchQueries": ["product launch review"],
"maxVideosPerSource": 5,
"onlyCreatorHearted": true,
"maxRepliesPerComment": 0,
"maxTotalRows": 2000
}
Example: backward-compatible tree output
{
"videoUrls": ["https://www.youtube.com/watch?v=jNQXAC9IVRw"],
"maxComments": 20,
"includeReplies": true,
"maxRepliesPerComment": 50,
"outputMode": "tree"
}
Use cases
- Sentiment and topic analysis — export flat comment and reply rows directly into a notebook or BI tool
- Creator engagement audits — measure which comments the creator pinned, hearted, or replied to
- Audience research — mine questions and recurring themes across a channel's recent uploads or a playlist
- Moderation datasets — collect long reply threads with parent/root IDs for context-aware classifiers
- Campaign monitoring — pull the last few days of comments from several videos with a date cutoff and minimum-like floor
- Competitive research — compare comment volume and engagement on the same topic across search results
FAQ
Do I need a YouTube API key or cookies?
No. The actor reads publicly available comment data without login, user cookies, or an API key. It sends YouTube's standard consent cookie so the EU consent page does not replace the video page.
Why are some comments missing?
YouTube's "Top comments" feed exposes a relevance-ordered subset (roughly 800–1,200 comments on very large videos). Use sortBy: "newest" for complete date windows and deeper coverage. The actor also continues with the newest feed when the top feed is exhausted, and maxComments, maxTotalRows, and your filters can all stop a run earlier than the source total.
Are replies complete?
The actor follows every reply continuation until the thread token is exhausted when maxRepliesPerComment: 0. YouTube can hide deleted, spam, or moderated replies, so the advertised replyCount may be higher than the rows returned. repliesFetched tells you exactly how many were emitted for a comment.
Why are timestamps approximate?
The public comment payload only contains relative labels such as "2 days ago". The actor converts each label into a date range with a precision (hour, day, week, month, year) and exposes the earliest and latest possible time. It never claims an exact timestamp.
Why do like counts end in round numbers?
YouTube exposes formatted counts (1.2K, 69K, 4.8M). likeCount is the rounded integer and likeCountText preserves the original string.
What does a date cutoff do?
commentsSince forces the newest-first feed, keeps pinned comments, and stops paginating at the first non-pinned comment older than the cutoff. Because dates are approximate, a comment whose range overlaps the cutoff is kept.
What happens if a video is private, age-restricted, or has comments off?
The actor classifies the page and, when emitStatusRows is enabled, writes one status row explaining the outcome. It does not try to bypass login, age, or region restrictions.
What is the difference between flat, tree, and both?
flat emits one row per comment and reply. tree emits parent comment rows with a replies[] array (useful for older integrations). both emits flat reply rows and also attaches replies[] to each parent.
Do I need to configure a proxy?
No. The actor chooses a working connection to YouTube automatically. Just fill in the input and run it.
Is this affiliated with YouTube?
No. This is an independent tool and is not affiliated with, endorsed by, or sponsored by YouTube or Google.
Limitations
- Exact comment timestamps and exact like counts are not available in the public payload.
- Nested replies beyond direct level 1 are not exposed in the observed public payload;
maxReplyDepthabove 1 is reserved for future support. - The top-comments feed is capped by YouTube; use newest-first plus a date cutoff for exhaustive windows.
- Age-restricted, private, members-only, and region-blocked videos are reported as statuses, not bypassed.
replyCountis advertised, not guaranteed; userepliesFetchedfor the delivered count.
Python quickstart
Use a schema-derived input and the Python standard library to start a run. An Apify account and token are required for execution.
Source build 0a5GXomczgZPh2iFX. Documentation is regenerated from the default published build; private drafts are not included.