TikTok's Research, Display and scopes docs offer ordinary apps no comments call; the one comment list is in the Research API, for approved researchers. ToolzerHub's /v2/tiktok/app/video/comments returns a public video's comments by video ID (we tested NASA and NatGeo videos), but its has_more flag turns false long before the end, so you step the cursor yourself. Done that way, a NASA video gave 2,513 rows against the 2,490 TikTok showed, but two other videos returned about 79% and 90%, and reply calls are most of the cost. We read TikTok's documentation and ran ours on 2026-10-17.
Does TikTok have a comments API?
Only for researchers. The Research API's Query Video Comments page (last updated September 1, 2026) lists the fields "id, video_id, text, like_count, reply_count, parent_comment_id, create_time". "Requesting video_id will return data for the specified video's comments", and "Requesting comment_id will return data for the specified comment's replies." max_count is "Default is 10, max is 100", and "It is possible that the API returns less comments than the max count due to reasons such as comments deleted by users etc."
The FAQ answers "I am a creator, advertiser, or commercial user. Am I eligible for access to the Research Tools?" with "No." Approved projects get "1000 requests per day". TikTok's scopes reference for ordinary apps covers profile info, video.list and publishing, with no comment scope. Our Research API post covers who can apply.
Get a video's comments with ToolzerHub
We make ToolzerHub. Our TikTok API returns comments for a public video by its ID, with one API key and no TikTok app. GET /v2/tiktok/app/video/comments takes aweme_id, cursor and count, returns 20 comments a page by default and up to 50 with count=50, with likes, reply counts and timestamps, and costs 2 credits a call either way. GET /v2/tiktok/app/video/comments/replies takes item_id (the video) and comment_id and pages a comment's replies (our OpenAPI file, 2026-10-17). An unknown video ID returns HTTP 200 with no comments.
The catch is the end of the list. On a NASA video showing 2,490 comments, following has_more and the returned cursor stopped after 17 pages and 283 top-level comments. Asking for cursor 340, 600 or 1000 directly still returned full pages. So the script below asks for 50 a page, ignores has_more, adds 50 to the cursor each time, and stops after three empty pages in a row. It takes video IDs or full video URLs, not vm.tiktok.com short links.
import csv
import os
import sys
import time
from datetime import datetime, timezone
import requests
BASE = "https://api.toolzerhub.com/v2/tiktok/app/video"
HEADERS = {"x-api-key": os.environ.get("TOOLZERHUB_API_KEY", "")}
FIELDS = ["video_id", "comment_id", "reply_to", "posted_at", "likes", "replies", "text"]
PAGE = 50 # count=50 returns up to 50 comments for the same 2 credits
calls = 0
def cell(value):
# Spreadsheets run text starting with = + - or @ as a formula; an apostrophe keeps it text.
if isinstance(value, str) and value[:1] in ("=", "+", "-", "@", "\t"):
return "'" + value
return value
def get(path, params, tries=4):
# 429 and 503 are retried; the wait comes from the error body or the Retry-After header.
global calls
for attempt in range(tries):
r = requests.get(BASE + path, headers=HEADERS, params=params, timeout=90)
calls += 1
if r.status_code == 400: # missing or empty ID: skip this video
print("bad request:", str(r.json()["error"].get("message")).split("\n")[0])
return None
if r.status_code not in (429, 503) or attempt == tries - 1:
if r.status_code >= 500: # any 5xx, or a 503 past the retries: skip
print(f"HTTP {r.status_code}, skipping:", r.text[:120].replace("\n", " "))
return None
r.raise_for_status()
return r.json().get("data")
try:
wait = r.json()["error"]["details"]["retryAfter"]
except (ValueError, KeyError, TypeError):
wait = None
try:
wait = int(wait or r.headers.get("Retry-After") or 5 * (attempt + 1))
except ValueError:
wait = 5 * (attempt + 1)
time.sleep(min(wait, 60))
def write(writer, video_id, c, seen, reply_to=""):
if not c.get("cid") or c["cid"] in seen: # pages overlap: keep one row per comment
return False
seen.add(c["cid"])
writer.writerow({
"video_id": video_id, "comment_id": c["cid"], "reply_to": reply_to,
"posted_at": datetime.fromtimestamp(c["create_time"], timezone.utc).isoformat()
if c.get("create_time") else "",
"likes": c.get("digg_count"), "replies": c.get("reply_comment_total"),
"text": cell((c.get("text") or "").replace("\r", " ").replace("\n", " ")),
})
return True
def export_video(writer, video_id, max_pages, with_replies):
seen, cursor, pages, empty, total, parents = set(), 0, 0, 0, None, []
status = "page cap reached"
while pages < max_pages:
data = get("/comments", {"aweme_id": video_id, "cursor": cursor, "count": PAGE})
if data is None:
status = "stopped early: failed page" if pages else "nothing returned"
break
pages += 1
total = total or data.get("total")
rows = data.get("comments") or []
if not rows and not seen and not total: # unknown or comment-free video
status = "nothing returned"
break
for c in rows:
if write(writer, video_id, c, seen) and int(c.get("reply_comment_total") or 0):
parents.append(c["cid"])
empty = 0 if rows else empty + 1
if empty >= 3:
status = "end"
break
# has_more can turn 0 long before the end, so step the cursor ourselves.
cursor += PAGE
time.sleep(0.3)
top = len(seen)
for parent in parents if with_replies else []:
params = {"item_id": video_id, "comment_id": parent, "cursor": 0}
while True:
data = get("/comments/replies", params)
rows = (data or {}).get("comments") or []
new = [write(writer, video_id, c, seen, parent) for c in rows]
nxt = (data or {}).get("cursor")
done = not (data and data.get("has_more") and nxt)
if done or nxt == params["cursor"] or not any(new):
break # also stops on a cursor that does not move or a page with nothing new
params["cursor"] = nxt
time.sleep(0.3)
shown = f"; TikTok shows {total}" if total else ""
print(f"{video_id}: {top} comments, {len(seen) - top} replies, {pages} pages "
f"({status}{shown})")
return len(seen)
def main(videos, max_pages="150", replies="no", out="tiktok_comments.csv"):
total = 0
with open(out, "w", newline="", encoding="utf-8-sig") as f: # BOM: Excel reads emoji
writer = csv.DictWriter(f, fieldnames=FIELDS)
writer.writeheader()
for v in [v.strip() for v in videos.split(",") if v.strip()]:
video = v.split("?")[0].split("#")[0].rstrip("/").split("/")[-1] # ID or full video URL
total += export_video(writer, video, int(max_pages), replies == "yes")
print(f"wrote {total} rows to {out} in {calls} calls")
if __name__ == "__main__":
if len(sys.argv) < 2 or (len(sys.argv) > 3 and sys.argv[3] not in ("yes", "no")):
sys.exit("usage: python tiktok_comments.py VIDEO_ID_OR_URL[,...] [max_pages] "
"[yes|no replies] [file.csv]")
if not HEADERS["x-api-key"]:
sys.exit("set TOOLZERHUB_API_KEY first")
main(*sys.argv[1:])
A 400 skips that video. A 429 or 503 is retried with the wait from retryAfter or Retry-After; a 503 that keeps coming back, or any other 5xx, skips the video, and a 429 that outlasts the retries, any other 4xx or a network error stops the run. The replies loop stops when the cursor stops moving or a page adds nothing new. Rows are de-duplicated by comment ID, because pages overlap. The CSV holds no commenter names; add them from the response if your use allows it. Text starting with =, +, - or @ gets a leading apostrophe, which stays visible in the CSV. We tested those branches against a stand-in server, not live.
Run it
Pass video IDs or video URLs, a page cap and yes for replies:
export TOOLZERHUB_API_KEY=your_key
python tiktok_comments.py "7691827402988686606, https://www.tiktok.com/@nasa/video/7679483952507276558?lang=en ,,7000000000000000001" 150 yes tiktok_comments.csv
Output on 2026-10-17, with a stray space, a full URL with a query string, an empty item and an unknown ID:
7691827402988686606: 68 comments, 48 replies, 5 pages (end; TikTok shows 112)
7679483952507276558: 1556 comments, 957 replies, 37 pages (end; TikTok shows 2490)
7000000000000000001: 0 comments, 0 replies, 1 pages (nothing returned)
wrote 2629 rows to tiktok_comments.csv in 262 calls
It took 182 seconds: 43 comment-page calls and 219 replies calls. The first run of the script, at 20 a page, needed 96 comment-page calls and got 93 rows on the small video instead of 116.
What we measured
On 2026-10-17, against the comment count TikTok showed on the first page. The middle column follows has_more at 20 a page; the right column steps the cursor at 50 a page.
| Video | Shown | Following has_more | Stepping, 50 a page |
|---|---|---|---|
| NatGeo, small | 112 | 55 + 42 replies | 68 + 48 replies |
| NASA | 384 | 238 + 70 replies | 276 + 70 replies |
| NatGeo | 706 | 240 + 124 replies | 421 + 134 replies |
| NASA, large | 2,490 | 283 + 735 replies | 1,556 + 957 replies |
Replies in the middle column are the counts the comments report. In the right column, the small and large videos count replies actually fetched, from the script run above; the 384 and 706 rows use the reported count.
- Following
has_moreended after 16 or 17 pages on the three longer videos. Stepping the cursor recovered the rest on the large video and most of it on the others; on the 706 one, cursor 700 came back empty, so the gap is not a stop we caused. - Pages overlap: a stepped run at 20 a page on the 706 video saw 125 repeats in 33 calls. The script keeps one row per comment ID.
totaldrifts near the end of a stepped run, to values like 1,700 and 1,740 that match the cursor, so the script keeps the figure from the first page.- On the three most-replied comments we paged, the replies route returned 152 for 88 stated, 57 for 51, and 44 for 45.
- A web route,
/v2/tiktok/web/post/comments, also ends early; paged with 50 per call it reached 1,274 top-level comments on the large video. - Reliability: 362 calls in the 7 days to 2026-10-17, 360 successful and none a server error, nearly all of them ours. Before our tests: one call. A first-page check on 10 videos returned comments every time.
What it costs
2 credits a call, at $0.99 to $1.90 per 1,000 credits ($1.45 on the $29 plan). The run above made 262 calls, 524 credits, for 2,629 rows: $0.52 to $1.00, about 199 credits per 1,000 rows. Replies cost the most calls, since most comments with replies need their own call. Without replies, the large video took 37 calls, 74 credits. The free 100 credits a month cover 50 calls, about 2,000 top-level comments at 50 a page. Failed calls cost nothing.
Where TikTok's API is the better pick
If you qualify for the Research API, use it: it is TikTok's own route, returns up to 100 comments a request, and its parent_comment_id field ties each reply to its parent; it carries the same note that it can return fewer comments than asked. Ours reads public videos only, can't post or moderate comments, and on some videos still misses part of the thread. For a few hundred comments from one video with no key, a free Python script pages TikTok's own comment endpoint; scraping TikTok with Python shows where it stops. For comments on Instagram, see getting Instagram comments.
We are not affiliated with TikTok or ByteDance.
Sources
- TikTok for Developers, Research API Query Video Comments, https://developers.tiktok.com/doc/research-api-specs-query-video-comments/, last updated 2026-09-01, read 2026-10-17
- TikTok for Developers, Research API FAQ, https://developers.tiktok.com/doc/research-api-faq/, last updated 2026-09-01, read 2026-10-17
- TikTok for Developers, Scopes Reference, https://developers.tiktok.com/doc/tiktok-api-scopes/, read 2026-10-17
- ToolzerHub OpenAPI file, https://api.toolzerhub.com/openapi.json, checked 2026-10-17
- ToolzerHub pricing, https://toolzerhub.com/pricing, checked 2026-10-17