Skip to content

Repository files navigation

YouTube Comments Scraper

YouTube Comments Scraper

YouTube Comments Scraper for extracting comment text, authors, like counts, reply counts and timestamps from any YouTube video. This repo has a free YouTube comments web scraping script you can run right now, and a YouTube comments data API that returns 10 fields per comment with cursor pagination.

This is the comments endpoint on its own. The YouTube Scraper repo covers the rest of the surface: search, videos, channels, playlists, transcripts and Shorts.

This repo reads public videos only, and never publishes a comment. Comments are written by members of the public, so no comment body or author display name appears anywhere in this repo: the committed sample is the response envelope with the comments array cut to 0 rows, the field reference is a schema table, and the runnable scripts write rows to a gitignored file instead of your terminal. More on that in Scope.

Last updated: 2026-07-20. Working against YouTube.com as of July 2026, and re-verified whenever YouTube changes their markup.

Below is the YouTube Comments Scraper to get you started. The envelope shown was captured from the live API on 2026-07-20; the cursor token is truncated where marked and the comment rows are cut to 0, but every envelope field is verbatim. The envelope sample is committed in youtube_comments_scraper_api_data/. Every code example calls the actual API and is runnable from youtube_comments_scraper_api_codes/.

pip install requests
export CHOCODATA_API_KEY="your_key"     # free: 1,000 requests, one-time, no card
python youtube_comments_scraper_api_codes/comments.py

Those three lines return the envelope below plus 20 comment rows, live from YouTube.com:

{
  "video_id": "4czjS9h4Fpg",
  "page": 1,
  "sort_applied": "top",
  "comment_count": null,
  "results_count": 20,
  "has_more": true
}

Each row in the comments array carries 10 fields. This is the shape, not the data, because the data belongs to the people who wrote it:

The 10 fields on each comment row

That is the whole point of this repo. The rest of this page is the reference: the parameters, the real envelope, and the fields worth knowing about before you build on them.


Contents


Scope: public videos, no published comments

The endpoint returns comments, and a comment is personal data: a display name, an author channel id, and a body written by a member of the public. This repo documents the endpoint without ever publishing one.

Every example and capture here targets a public institutional video: NASA's official Perseverance landing (4czjS9h4Fpg), where the comments are the public reacting to a public brand. Even so:

  • The committed sample (comments_envelope.json) is the response envelope with the comments array cut to 0 of 20 rows. The 8 envelope fields are verbatim; no comment row is committed.
  • The field reference below is a schema table (field, type, description), not a data dump.
  • The runnable scripts print the envelope and per-page aggregates, and write comment rows to a gitignored comments.jsonl, so a comment body never lands in your scroll-back, a screenshot, or a CI log.

If you point the endpoint at your own audience, the rows are your responsibility. Keep them out of shared logs the same way these scripts do.

Free YouTube Comments Scraper

YouTube does not put comments in the watch page HTML. The page ships a continuation token instead, and the browser POSTs that token to YouTube's own youtubei/v1/next endpoint to load the first comment page as JSON. You can do the same two steps with requests, no headless browser and no JavaScript rendering. No key, no cost:

python free_scraper/youtube_comments_free_scraper.py "https://www.youtube.com/watch?v=4czjS9h4Fpg"

Source: free_scraper/youtube_comments_free_scraper.py. It reads the INNERTUBE_API_KEY, the client version and the comments continuation token out of the watch page's ytInitialData, POSTs the token to youtubei/v1/next, and parses the comment rows. Rows are written to a gitignored comments.jsonl; only counts reach the terminal.

After running the command, your terminal should look something like this:

Free YouTube comments scraper parsing a real video

It works. The two-hop path returned comments on 6 of 6 consecutive attempts in a measured run on 2026-07-20, 4 seconds apart: a ~1.19 MB watch page followed by a ~277 KB next response, 20 comments each. Note that marker strings like captcha and consent.youtube appear in the JavaScript of perfectly healthy watch pages, so the script only calls something a block once the parser finds no comments, never on a substring match.

What actually costs you time on comments is a different set of problems, and it is the subject of the next section.

Avoid getting blocked when scraping YouTube comments

Getting blocked is not the interesting failure mode here. These are, and all of them are measured against YouTube on 2026-07-20.

First, comments are not in the page you fetch. A watch page is ~1.19 MB of HTML and none of it is a comment. The comments live behind a second request to youtubei/v1/next using a token minted in the first. So the minimum to read page 1 is two hops, and a scraper that only fetches the watch page gets zero comments while looking like it succeeded (HTTP 200, a full 1 MB body).

Second, sort=newest usually cannot be honoured on page 1. The "Newest first" sort needs a sub-menu token that YouTube ships only after the first comment page loads, so it is absent from the initial page. Ask for newest and the response tells you what it actually did: on 4czjS9h4Fpg, sort=newest came back with sort_applied: "top". Read sort_applied, do not assume the sort you asked for is the sort you got.

Third, comment_count is usually null. The header count loads in a later continuation, not the first next response, so comment_count was null on all three videos sampled (4czjS9h4Fpg, dQw4w9WgXcQ, jNQXAC9IVRw). The field exists; it is just empty on page 1 more often than not.

Here is the full picture, and what each item costs you:

What bites you Why What it costs you
Comments are a second request The watch page carries a continuation token, not the comments. You POST it to youtubei/v1/next to get them. A one-hop scraper returns 200 and zero comments. The failure looks like success.
sort=newest falls back to top The newest-sort token is absent from the initial page. If you assume newest, you silently analyse top-ranked comments instead. sort_applied is the only truth.
comment_count is null on page 1 The header count arrives in a later continuation. You cannot read a video's total comment count from page 1. Null on 3 of 3 videos sampled.
reply_count is null, not 0 A comment with no replies carries reply_count_text: "", which parses to null. sum(reply_count) throws on null unless you coalesce. 5 of 20 rows on the sample page were null.
top is the same 20 every call Top ordering is stable within a session. Re-calling with the same params returns identical rows. New comments come from the cursor, not from polling.

So the two paths, side by side, same video, same day:

Free YouTube comments scraper vs Chocodata API, measured

Both make the same two hops to reach the comments. The difference is what you carry: a 1.19 MB watch page plus a 277 KB continuation you parse and re-walk yourself, versus one GET that returns the envelope, the 20 parsed rows and the next-page cursor.


Using the Chocodata YouTube Comments Scraper API

The managed option, and the one this repo is built around: the Chocodata YouTube Comments Scraper API. One GET request per comment page, 10 fields per comment of YouTube comment data extraction at scale, a ~99% success rate, and no proxy management. The two-hop fetch and the continuation cursor are handled for you. Free for the first 1,000 requests.


YouTube Comments Scraper API reference

Below is the YouTube Comments Scraper API reference to get you started: authentication, the error bodies, the rate limits, and the endpoint itself.

Quickstart

curl "https://api.chocodata.com/api/v1/youtube/comments?api_key=YOUR_KEY&video_id=4czjS9h4Fpg&sort=top"
import requests

r = requests.get(
    "https://api.chocodata.com/api/v1/youtube/comments",
    params={"api_key": "YOUR_KEY", "video_id": "4czjS9h4Fpg", "sort": "top"},
    timeout=90,
)
data = r.json()
print(data["video_id"], data["sort_applied"], data["results_count"], data["has_more"])
# 4czjS9h4Fpg top 20 True

After running the command, your terminal should look something like this. The envelope and aggregates print; the rows go to a gitignored file:

Running the YouTube Comments Scraper API

Authentication

Pass your key as the api_key query parameter. There is no header form and no OAuth step.

https://api.chocodata.com/api/v1/youtube/comments?api_key=YOUR_KEY&video_id=4czjS9h4Fpg

A free key is 1,000 requests, one time, with no card. Get one at chocodata.com.

Errors

Nothing below is billed: you are only charged on a 2xx.

Status error code Meaning Billed What to do
400 invalid_params None of video_id, url or page_token was supplied. The body names the missing param and its path. no Fix the query string.
401 INVALID_API_KEY Key missing, unrecognised, or revoked. no Check api_key. Get one at chocodata.com.
402 INSUFFICIENT_CREDITS Balance exhausted. no Top up or upgrade.
429 RATE_LIMITED Over 120 requests/60s, or over your plan's concurrency. no Back off and retry.
502 - YouTube did not return parseable comments, or comments are disabled on this video. no Confirm comments are on, then retry once after a few seconds.

A bad key, verbatim:

curl "https://api.chocodata.com/api/v1/youtube/comments?api_key=totally_invalid_key&video_id=4czjS9h4Fpg"
{"error": {"code": "INVALID_API_KEY", "message": "Api key not recognised."}}

A missing required param, verbatim. Note that it names the real parameter, which is the fastest way to find out the endpoint takes video_id and not id:

{"error": "invalid_params", "issues": [{"code": "custom", "message": "youtube.comments requires `video_id`, `url`, or `page_token`", "path": ["video_id"]}]}

Two response shapes exist: auth and billing errors nest under error.code (uppercase), while param errors are flat with a lowercase error string and an issues array. A video with comments disabled returns 502, not an empty 200. The scripts in this repo map the documented statuses onto actionable messages, so a typo'd key does not hand you a stack trace:

YouTube Comments Scraper API error handling

Rate limits and concurrency

Two separate limits apply, and they are enforced independently.

Limit Value
Requests per key 120 per 60 seconds (sliding window)
Concurrent requests, Free 10
Concurrent requests, Vibe 30
Concurrent requests, Pro 50
Concurrent requests, Custom 100

Exceed either and you get 429, not a queue. Every call is a synchronous GET: there is no webhook, callback, or async job to poll. Paging one video is sequential because each page_token comes from the previous page. The examples use timeout=90 and keep well under ~1.5 requests per second.

Comments: text, authors, likes, replies and timestamps

One page of about 20 comments for a video, plus the pagination cursor and, when the page carries it, the total comment count.

Param Type Required Default Description
video_id string one of video_id/url - The 11-char watch id (e.g. 4czjS9h4Fpg), not id.
url string (URL) one of video_id/url - Any video URL form (watch?v=, youtu.be/, /shorts/, /embed/, /live/); the id is parsed out.
sort enum no top top or newest. newest needs a token that is usually absent from page 1, so it falls back to top; read sort_applied for what was used.
country string (ISO-2) no US Two-letter country the request is routed from.
page_token string no - The next_page_token from a previous call. When present, no video_id is needed and exactly one further page is returned, carrying the same sort.
api_key string yes - Your key. Query parameter, not a header.
curl "https://api.chocodata.com/api/v1/youtube/comments?api_key=YOUR_KEY&video_id=4czjS9h4Fpg"

Real response envelope. Every envelope field is present and verbatim except the two cursor fields, truncated where marked; the comments array is cut to 0 of 20 rows, because those rows are written by members of the public and this repo does not publish them (committed sample, same cut):

{
  "video_id": "4czjS9h4Fpg",
  "url": "https://www.youtube.com/watch?v=4czjS9h4Fpg",
  "page": 1,
  "sort_applied": "top",
  "comment_count": null,
  "comment_count_text": null,
  "results_count": 20,
  "comments": [],
  "next_page_token": "eyJjIjoiRWcwU0N6UmplbXBUT1dn...(truncated)",
  "next_page_url": "/api/v1/youtube/comments?page_token=eyJjIjoiRW...(truncated)",
  "has_more": true
}

Each entry in comments carries these 10 fields:

Field Type Description
id string Opaque comment id. Also the pagination anchor for this row.
text string The comment body.
author string Author display name.
author_channel_id string Author channel id. The stable join key for an author, present on all 20 rows sampled.
author_is_verified boolean Verified badge. 3 of 20 were verified on the page sampled.
published string Relative age, e.g. "5 years ago" or "5 years ago (edited)". Not a date.
like_count integer Likes, expanded from the display text. Ranged 7 to 58,000 on the sample page.
like_count_text string The display form, e.g. "1.2K", "58K".
reply_count integer or null Replies. null when the comment has no replies, not 0. Null on 5 of 20 rows sampled.
reply_count_text string The display form; "" when there are no replies.

text and author are what most people come for, and they are exactly what this repo will not print for you. The comments.py example writes rows to a gitignored comments.jsonl and prints only the envelope and aggregates. Field notes, each measured on 2026-07-20:

  1. sort_applied is authoritative. sort=newest fell back to top on the sample video. Branch on sort_applied, not on what you requested.
  2. comment_count is null on page 1 across all 3 videos sampled. Do not read a video's total from it.
  3. reply_count is null for reply-free comments. Coalesce (reply_count or 0) before you sum, or the arithmetic throws.
  4. top is deterministic within a session. Three consecutive top calls returned the same 20 ids in the same order, so pagination, not re-calling, is how you gather more.
  5. Pagination does not repeat. Page 2 (fetched from next_page_token) shared 0 ids with page 1.

A page-2 envelope is committed at comments_page2_envelope.json, also cut to 0 rows, showing the slimmer envelope a token page returns (no url, comment_count or comment_count_text).

Runnable: youtube_comments_scraper_api_codes/comments.py


Page through a video's comments and summarise engagement

The job most people arrive with: read many pages of a video's comments and get aggregate numbers out, without dumping personal data to a terminal.

export CHOCODATA_API_KEY="your_key"
python youtube_comments_scraper_api_codes/paginate_comments.py 4czjS9h4Fpg --pages 5

It follows next_page_token for N pages, appends rows to a gitignored comments.jsonl, and prints only totals: comment count, verified authors, and the summed likes and replies. Five pages of 4czjS9h4Fpg gathered 100 comments with 0 repeats:

Paging through a video's comments

Because top is deterministic, a second call with the same params buys nothing; new comments come from the cursor. The script sleeps 1.5s between pages to stay inside the 120 requests/60s limit and retries a lone 502 once after 8 seconds.

Source: youtube_comments_scraper_api_codes/paginate_comments.py


Measured latency

Ten consecutive calls to /youtube/comments for the same video id, 3 seconds apart, on 2026-07-20. All ten returned 200:

Measured latency for the YouTube comments endpoint

Metric Value
calls attempted 10
200 responses 10
min 2,441 ms
median 2,808 ms
max 4,361 ms

Ten calls is a small sample, which is why the examples set timeout=90 rather than sizing to the median, and why the pagination script retries a 502 once before giving up.


License

MIT. See LICENSE.