By the end of this page you have a seventy-line Python script and a JSONL file holding every public comment and reply from one video, plus a flag that proves the walk finished. YouTube comments are messy, but they're full of useful signal. Viewers say what confused them and describe products in language you would never get from a survey. They also have a knack for repeating the same complaint in wildly different words. Put all of that in one dataset and you can measure sentiment, monitor reactions to a competitor's videos, or turn recurring questions into your next content brief.
How a comment section is shaped
Public YouTube comment data has two levels that paginate separately. A top-level comment is posted directly on the video. That comment and its replies form a thread. A reply belongs to one top-level comment; replies do not nest further.
Each level hands you its own cursor. A page of comments carries pagination.next_cursor, which walks to the next page of threads. Each thread separately carries a replies_cursor, which opens that one thread's replies. A thread with no replies has a null replies_cursor, so you can skip it without spending a request.
{"video_id": "BXmOlCy0oBM","returned_count": 20,"threads": [{"comment": { "comment_id": "Ugyf7z...", "text": "six seven", "author": "@strolol" },"replies_cursor": null}],"pagination": {"next_cursor": "eyJ2IjoxLCJ2aWQiOiJCWG1PbEN5MG9CTSIs...","completion": null}}
YouTube's displayed comment count can include replies, so it will not match the number of top-level threads in the output. Keep thread and reply totals separate. Their sum is a useful sanity check, but it is not proof of completeness because the visible count can change while a long run is in progress.
The script
Seventy lines, no dependencies beyond requests, and it assumes the happy path: every call succeeds and the run finishes in one go. That is the version worth reading first. You need Python 3.9 or newer and a Tapline API key.
pip install requestsexport TAPLINE_API_KEY=sk_live_...
import jsonimport osimport sysimport timeimport requestsBASE = "https://api.tapline.sh/api/v1/youtube"PAGE_SIZE = 20PAUSE = 1.05 # stays inside the Free plan's 60 requests a minutesession = requests.Session()session.headers["X-API-Key"] = os.environ["TAPLINE_API_KEY"]def get(path, **params):response = session.get(f"{BASE}{path}", params=params, timeout=60)response.raise_for_status()time.sleep(PAUSE)return response.json()def comment_pages(video_id):page = get(f"/videos/{video_id}/comments", sort="new", limit=PAGE_SIZE)while True:yield pagecursor = page["pagination"]["next_cursor"]if not cursor:returnpage = get(f"/videos/{video_id}/comments", cursor=cursor, limit=PAGE_SIZE)def all_replies(video_id, comment_id, cursor):replies = []while cursor:page = get(f"/videos/{video_id}/comments/{comment_id}/replies",cursor=cursor,limit=PAGE_SIZE,)replies.extend(page["replies"])cursor = page["pagination"]["next_cursor"]return repliesdef scrape(video_id, out_path):threads = replies = 0completion = Nonewith open(out_path, "w", encoding="utf-8") as out:for page in comment_pages(video_id):for thread in page["threads"]:comment = thread["comment"]comment["replies"] = all_replies(video_id, comment["comment_id"], thread["replies_cursor"])out.write(json.dumps(comment, ensure_ascii=False) + "\n")threads += 1replies += len(comment["replies"])completion = page["pagination"]["completion"]print(f" {threads} threads, {replies} replies", end="\r")return threads, replies, completionif __name__ == "__main__":video_id = sys.argv[1]threads, replies, completion = scrape(video_id, f"{video_id}.jsonl")print(f"\n{video_id}: {threads} threads, {replies} replies, completion={completion}")
The script stops on the first failed call. For a long run, wrap the request in a small retry with backoff for 429 and 5xx responses, or rerun it and de-duplicate on comment_id.
The example video is Erlang: The Movie, whose comment section is small enough to finish in a few seconds.
$ python scrape_comments.py BXmOlCy0oBMBXmOlCy0oBM: 27 threads, 12 replies, completion=exhausted
You get one JSONL row per top-level comment, with that comment's replies nested inside it. This row from the live comments demo is trimmed to the fields most people use:
{"comment_id": "Ugyf7zGFi5QRSGoS3dl4AaABAg","text": "six seven","like_count": 0,"published_at": "2026-05-06T14:36:13Z","is_pinned": false,"author": "@strolol","author_is_uploader": false,"replies": []}
One line per thread keeps the output streamable for pandas, DuckDB, and jq, and a row is only written once that thread's replies are all in hand.
How it works
Top-level pagination
The first request sends sort=new and limit=20. Each later request sends the response's next_cursor back as cursor. The cursor already represents the video and sort order, so the script does not mix first-page parameters into later requests. A cursor from another walk may return a 400 response with an invalid_cursor error.
Reply pagination
Reply pagination starts with the replies_cursor attached to its parent thread. Threads with a null cursor need no reply request. Otherwise, the replies endpoint follows its own next_cursor until it is null. Each thread has a separate reply walk, so the distribution of replies across threads affects request count more than the total reply count alone.
Checking the result
The run above returned 27 top-level threads and 12 replies against a video that YouTube displayed as having 39 comments when this tutorial was last updated. The totals agree, but the more reliable signal is pagination.completion on the last response. Both limit and sort settings can produce a terminal response without full coverage. The YouTube API docs show the cursor and completion fields used here.
Two settings can quietly leave the walk incomplete. A limit below 20 still moves the cursor by a full 20-thread page, so the walk skips threads it did not return. The top sort can also stop issuing continuation tokens well before the end, which the final response reports as completion: depth_limit. Sort by new when you want everything.
FAQ
No. It collects comments and replies available on the publicly visible YouTube page during the run. It cannot return private, deleted, held-for-review, or otherwise unavailable comments.
Read pagination.completion on the final response. It is exhausted when the walk reached the end of the comment section and depth_limit when YouTube stopped issuing continuation tokens first, which happens on the top sort. Sort by new to avoid that cap, and keep limit at 20, because the cursor advances a full 20-thread page no matter how few you ask for.
YouTube displays threads and replies added together, while the comments endpoint counts only top-level threads. A video showing 39 comments returns 27 threads plus 12 replies. Compare threads plus replies against the number on the video page to check a run finished.
No. A walk over a large comment section can span changes to the public page while it runs. Store the run start and finish times alongside the output, and reconcile repeated snapshots on comment_id. Comment timestamps include a time component, but they do not turn a long walk into a single snapshot.
If you also need transcripts, compare the hosted options in the roundup.