How to get TikTok comments programmatically
TikTok's public developer products do not give you comments on an arbitrary video URL. The working call is GET /v1/tiktok/comments: pass the watch URL, get comment text plus stable author ids and a cursor, for a flat 2 credits.
Every field name and value below comes from a production response captured on 17 October 2026 against a public Khaby Lame video. Nothing is paraphrased from docs.
What a comment page looks like
limit=3 on tiktok.com/@khaby.lame/video/7646812028874673439 returned three rows. totalComments on that video was 441. Trimmed:
{
"success": true,
"data": {
"platform": "tiktok",
"url": "https://www.tiktok.com/@khaby.lame/video/7646812028874673439",
"totalComments": 441,
"totalReturned": 3,
"comments": [
{
"id": "7647102586621297429",
"text": "С каждым лайком нос растёт",
"author": "gudingar",
"authorName": "Gudin",
"authorId": "6939890549002732550",
"authorSecUid": "MS4wLjABAAAAraX8crl84TxM03j_I4R4UMmeMknF6_D_QEpSSF1WRAFA1XHgGsDpop10w3QNkDF5",
"commentLanguage": "ru",
"likeCount": 709,
"publishedAt": "2026-06-03T09:43:54.000Z"
},
{
"id": "7646813993886073621",
"text": "Reposting everyone who likes this comment👍",
"author": "realjjfrosh",
"authorName": "Realjjfrosh",
"authorId": "7444971872276120631",
"commentLanguage": "en",
"likeCount": 16542,
"publishedAt": "2026-06-02T15:04:01.000Z"
}
],
"nextCursor": "3",
"hasMore": true,
"fetchedAt": "2026-10-17T13:21:14.940Z"
}
}
author is the handle; authorName is the display name — they are not the same person field. authorId and authorSecUid stay stable when someone renames. commentLanguage is on the row so a listening loop does not need a second detector; this page had ru and en. replyCount is included only when TikTok exposes it — none of these three rows had it. Replies are a different call: /v1/tiktok/comment-replies with the parent comment id.
The call
JavaScript — keep ids as strings:
const res = await fetch(
"https://api.captapi.com/v1/tiktok/comments?" +
new URLSearchParams({
url: "https://www.tiktok.com/@khaby.lame/video/7646812028874673439",
limit: "50",
}),
{ headers: { Authorization: `Bearer ${process.env.CAPTAPI_KEY}` } },
);
const { data } = await res.json();
for (const c of data.comments) {
console.log(c.publishedAt, c.commentLanguage, c.authorId, c.text);
}
if (data.hasMore) {
// pass data.nextCursor back as cursor
}
Python:
import httpx, os
r = httpx.get(
"https://api.captapi.com/v1/tiktok/comments",
params={
"url": "https://www.tiktok.com/@khaby.lame/video/7646812028874673439",
"limit": 50,
},
headers={"Authorization": f"Bearer {os.environ['CAPTAPI_KEY']}"},
timeout=60,
)
data = r.json()["data"]
for c in data["comments"]:
print(c["publishedAt"], c.get("commentLanguage"), c["authorId"], c["text"])
print("next", data.get("nextCursor"), "hasMore", data["hasMore"])
This capture came back in 4.5 s. limit goes up to 500; the credit cost stays 2 either way. hasMore is always a boolean. A null nextCursor is the end of the thread. Page by sending the previous nextCursor back as cursor — it is a numeric offset ("3" after three rows), not an opaque token. Pass cache=true for the 24h shared cache (0 credits on a hit); default is a live fetch.
Replies under one comment are not on this list. Call /v1/tiktok/comment-replies with the same video URL and the parent id. Same author fields, same 2-credit flat rate, same string-id rule. Do not invent a replyCount of 0 when the key is missing — this page omitted it.
The trap: the id is a string, not a number
Comment id 7647102586621297429 and author id 6939890549002732550 are 19-digit values. They are larger than JavaScript's Number.MAX_SAFE_INTEGER (9 007 199 254 740 991). The API returns them as strings on purpose. If you write Number(comment.id), or if you parse a raw TikTok payload where the same field arrives as a JSON number, the id you store is no longer the id TikTok issued — Number("7647102586621297429") === 7647102586621297429 is false. Dedup, reply lookups, and "this user commented twice" all break silently. Keep id, authorId, and the video id as strings end to end. Same class of bug as Instagram mediaId — the Reels listing has it too.
A missing video is a 404, not an empty list
The same endpoint on a video id that does not resolve:
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Video not found",
"retryable": false
}
}
That call was HTTP 404. It is not billed. Do not poll it. A 200 with comments: [] would mean the video exists and this page is empty; that is a different event. The miss above took 33 s — a 404 is not always fast.
When the native comment path is blocked, the first page can still arrive from the fallback path. Those responses set degraded: true and degradedReason: "extended". hasMore is then false: the fallback path cannot page. Retry later if you need a cursor. That path was not the one on this capture.
The comments endpoint page has the full field list and the replies sibling. A free key is 100 credits — enough to page a thread and hit a dead URL once to see the 404.