TikTok Comments Scraper By Real Comment Filter
Pricing
$19.99/month + usage
TikTok Comments Scraper By Real Comment Filter
TikTok Comments Scraper extracts comments from public TikTok videos at scale. Collect comment text, usernames, likes, replies, and timestamps. Ideal for sentiment analysis, audience research, moderation workflows, and social media insights.
Pricing
$19.99/month + usage
Rating
0.0
(0)
Developer
Scraper Engine
Maintained by CommunityActor stats
0
Bookmarked
16
Total users
1
Monthly active users
6 days ago
Last modified
Categories
Share
TikTok Comments Scraper — Spam Score, Media URLs, Duplicate Groups
TikTok Comments Scraper By Real Comment Filter extracts the comment section under any public TikTok video as structured JSON and cleans it before you are billed: the comment text, the commenter's uniqueId and commenterProfileUrl, diggCount, createTimeISO, decoded mediaUrls for picture and sticker comments, and an auditable spamScore with the exact spamSignals behind it. Sixty-five keys per row, no HTML parsing, no TikTok login. Paste video links or creator usernames and press Start.
⚠️ The filter is a rule, not a model — and the optional AI runs on your own provider bill
Two things get conflated in comment scrapers that mention spam, so they are separated here before anything else.
The "real comment filter" is deterministic and free. spamScore, spamRiskLevel and spamSignals are computed inside the Actor from data that is already in the comment payload — a link pattern in the text, the same text repeated elsewhere in the run, three or more @mentions, text that is nothing but emoji, and a commenter who appears three or more times in the run. Five weighted signals, no model, no network call, no key, no extra cost. hideLikelySpam filters on that number and nothing else. It works on every run, including a completely default one.
aiEnhancement is the only feature that calls an external model, it is off by default, and Apify does not bill it. Switching it on sends comment text to the provider implied by aiModel — Anthropic for claude-*, Google for gemini-*, OpenAI for gpt-* / o1 / o3, xAI for grok-*, DeepSeek for deepseek-*, Perplexity for sonar*, Mistral for mistral-*, and OpenAI as the fallback for any name that matches nothing. Those calls are charged to your account with that provider, on their pricing, entirely separately from your Apify usage. Comments go out in batches of 25 with each comment truncated to 400 characters, so token spend scales with how many comments survive your filters, not with how many videos you queue.
Comment text leaves Apify when AI enhancement is on. A TikTok comment is written by an identifiable person. Under GDPR that model provider becomes a processor in your processing chain: it belongs in your record of processing activities, needs to be covered by your controller-processor terms, and should appear where your privacy notice lists recipients and international transfers. With aiEnhancement off, no comment text goes anywhere except your own Apify dataset.
If AI is on and no key is found, the run does not fail — and the rows are still charged. The Actor reads aiApiKey first, then the provider's environment variable (ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY, XAI_API_KEY, DEEPSEEK_API_KEY, PERPLEXITY_API_KEY, MISTRAL_API_KEY). If both are empty it logs a warning and carries on: every comment is still scraped, still written and still charged as a row_result, with aiIsSpam, aiCategory and aiSpamReason all null. Unlike a silent no-op, the reason is written into the row — aiError reads no API key supplied, so you can detect it in the dataset rather than only in the log.
The model is called after the filters, never before them. De-duplication, the media filter, the spam filter and duplicate-text collapsing all run first, and only the rows that survive are sent to your provider. You never pay a model to classify a comment the Actor then discards.
A fake, spam or bot label is an inference about a person's post, not a fact. spamScore is a weighted rule of thumb and aiIsSpam / aiCategory / aiSpamReason are a language model's opinion of a truncated string. Both can be wrong about a real person's real comment. No accuracy figure is claimed anywhere in this README, for either layer, and the defaults ship with filtering off so the score stays advisory until you decide otherwise.
What is TikTok Comments Scraper By Real Comment Filter?
It is an Apify Actor that reads TikTok's public comment list endpoint for a video, returns one dataset row per top-level comment with replies nested inside it, and adds three layers the raw payload does not give you: duplicate comment IDs removed before anything is pushed or billed, image and sticker comments decoded into real URLs instead of arriving as blank text, and a per-row spam score whose individual signals are all visible as their own columns.
No TikTok account, login or session cookie is required, and the input schema accepts none. The Actor reads the same public endpoints a signed-out visitor's browser calls. An LLM provider key is needed only if you switch aiEnhancement on.
It is built for social listening and community teams triaging a noisy comment section, brand-safety and influencer analysts checking whether engagement is real, researchers assembling comment corpora, and developers or AI engineers feeding comment text into their own pipelines.
What TikTok comment data is publicly available to scrape?
TikTok's comment endpoint serves a signed-out visitor the full comment body and the commenter's public identity: text, likes, exact timestamp, handle, nickname, avatar, numeric user ID, the comment's own permalink, TikTok's language tag for the comment, and the image or sticker attached to it. What it withholds is anything belonging to a private or restricted video, and anything about the commenter beyond their public profile stub.
| Data Category | Signed-out visitor | Gated behind a TikTok login or the account owner |
|---|---|---|
| Comment text, like count, exact timestamp | ✅ Public | — |
Commenter handle, nickname, avatar, numeric user ID, sec_uid | ✅ Public | — |
| Comment permalink and TikTok's own comment language tag | ✅ Public | — |
Image and sticker comments (image_list, cmt_sticker_struct) | ✅ Public | — |
| Reply threads under a comment, and the video's total comment count | ✅ Public | — |
| TikTok's pinned, top-comment, author-liked and author-reply markers | ✅ Public | — |
| Comments on private, friends-only or age-restricted videos | ❌ | Login and eligibility — never returned |
| Comments the creator deleted, hid or filtered | ❌ | Not served to anyone |
| Commenter's follower count, bio, email or phone | ❌ | Not present in the comment payload at all |
| Whether a commenter is a bot | ❌ TikTok publishes no such flag | ❌ Inferred inside the Actor — see the spam and AI sections |
TikTok Comments Scraper By Real Comment Filter only returns publicly visible data — what any signed-out visitor sees under the video. Nothing behind a login wall. The duplicate, spam and AI columns are not fetched from TikTok at all; they are derived from the comments the endpoint already returned.
What data can I extract with TikTok Comments Scraper By Real Comment Filter?
Every charged row carries five things: the comment and who wrote it, the picture or sticker it contains, whether anyone else in the run wrote the same thing, a spam score with its individual signals exposed, and — only if you asked for it — a model's opinion. Sixty-five top-level keys are written on every row of every run.
The tables below are split by where each value comes from, because that distinction matters legally as well as analytically: what TikTok published, what the Actor computed from a rule, and what a language model wrote.
The comment, the commenter and the video — published by TikTok
| Field Name | Description |
|---|---|
cid | TikTok's comment ID. The de-duplication key, and the field to keep for subject-access requests |
text | The comment body, verbatim. "" on a picture-only comment — check isMediaOnlyComment before treating that as broken |
createTime | Comment timestamp as a Unix epoch integer |
createTimeISO | The same instant as YYYY-MM-DDTHH:MM:SS.000Z in UTC. "" when TikTok gave no usable timestamp |
diggCount | Likes on the comment |
replyCommentTotal | TikTok's own count of replies under the comment — the true total, independent of how many you fetched |
uid | The commenter's numeric TikTok user ID |
uniqueId | The commenter's handle, without the @ |
nickname | The commenter's display name, or null |
secUid | TikTok's opaque secondary user ID, or null |
commenterProfileUrl | https://www.tiktok.com/@{uniqueId}, built from the handle. null when TikTok returned no handle |
avatarThumbnail | First avatar thumbnail URL, or "" |
commentLanguage | TikTok's own language code for the comment, or null. Never defaulted to "en" |
commentLink | TikTok's share permalink for the individual comment, or null |
likedByAuthor | TikTok's is_author_digged flag — the creator liked this comment |
pinnedByAuthor | TikTok's author_pin flag |
isTopComment | Parsed from TikTok's sort_tags string — the comment sits in TikTok's top list |
isAuthorReply | Parsed from the same sort_tags string |
isHighPurchaseIntentFlag | TikTok's own is_high_purchase_intent flag, passed through raw and honestly named. It is TikTok's label, not a buying signal this Actor stands behind |
replyScore | TikTok's internal sort_extra_score.reply_score, or null |
showMoreScore | TikTok's internal sort_extra_score.show_more_score, or null |
foldStatus | TikTok's raw fold_status moderation value |
noShow | TikTok's raw no_show moderation flag |
userBuried | TikTok's raw user_buried moderation flag |
commentStatus | TikTok's raw comment status value |
repliesToId | The comment this one replies to, or null on a top-level comment |
videoId | The video's numeric ID, from the comment's aweme_id |
videoTotalComments | TikTok's total comment count for the video, taken from the first page. null on nested replies |
videoWebUrl | Canonical video URL, query string stripped, short links resolved |
submittedVideoUrl | A second copy of videoWebUrl — despite the name it holds the resolved URL, not the string you submitted |
input | The exact URL this row was collected from. In creator mode this is the discovered video URL, not the username |
The four raw moderation flags (foldStatus, noShow, userBuried, commentStatus) are passed through unmodified and were constant on the samples the Actor was built against. They are exposed so you can check them yourself, not because they are claimed to detect anything.
Image and sticker comments — decoded from TikTok's payload
A large share of a busy TikTok comment section is pictures and stickers, and those comments arrive from the API with an empty text. Left undecoded they look like blank or broken rows. These ten keys turn them into usable data.
| Field Name | Description |
|---|---|
mediaUrls | Array of full-size image URLs from image_list, preferring origin_url and falling back to crop_url. [] when there are none |
mediaThumbnails | Matching cropped previews, preferring crop_url and falling back to origin_url |
mediaDimensions | Array of { "width", "height" } objects, one per image. Both values are null when TikTok did not report them — never invented |
mediaCount | Number of images on the comment |
stickerUrl | Animated sticker URL from cmt_sticker_struct, falling back through mid, low and static resolutions. null when absent |
stickerId | The sticker's ID as a string, or null |
hasMedia | Rule-derived. true when the comment carries an image or a sticker |
mediaType | Rule-derived. "image", "sticker", "imageAndSticker" or null |
isMediaOnlyComment | Rule-derived. true when there is media and no text — the key that tells an empty text apart from a broken row |
isEmptyWithoutMedia | Rule-derived. true for a genuinely empty comment with no media either |
Duplicates and the spam score — computed inside the Actor, rule-derived
None of the fields below is published by TikTok. Every one is calculated from the comments this run already downloaded, across the whole run rather than per video — so duplicateTextCount and commenterCommentsInRun describe your run's dataset, not the video in isolation.
| Field Name | Description |
|---|---|
isDuplicateText | true when another row in this run has the identical text after case and whitespace normalisation |
duplicateTextCount | How many rows in the run share that exact text. 0 on empty-text rows, which are never grouped |
duplicateTextGroupId | Stable 1-based group ID, largest group first. null when the text is unique or empty |
isCollapsedDuplicateGroup | true only when collapseDuplicateTexts was on and this row is the surviving representative of a repeated-text group. false on every row otherwise |
mentions | Array of handles found in the text by regex. TikTok's own text_extra entity list measured empty on the live samples, so the regex is the real path rather than a fallback |
mentionCount | Length of mentions |
containsLink | true when the text matches a URL, a www. host, or a bare domain on a common TLD |
linkInText | The first matched link string, or null |
isMentionStuffed | true at three or more mentions |
isEmojiOnlyText | true when the text is non-empty and consists entirely of emoji, symbols and variation selectors |
commenterCommentsInRun | How many rows in this run share this uid |
isRepeatCommenterInRun | true at three or more comments from the same uid in this run |
spamSignals | Array naming exactly which signals fired: containsLink, repeatedTextInRun, mentionStuffing, emojiOnlyText, repeatCommenterInRun |
spamScore | 0–100. Weights: link 45, repeated text in run 30, mention stuffing 30, emoji-only 15, repeat commenter 15, summed and capped at 100 |
spamRiskLevel | "high" at 60 or above, "medium" at 30 or above, "low" above 0, "none" at 0. These thresholds are fixed and independent of spamScoreThreshold |
The weighting is the whole design: at the default spamScoreThreshold of 60, no single weak signal hides a row on its own, but two independent ones do. A link alone scores 45 and stays. A link plus emoji-only text scores 60 and is hidden once you switch hideLikelySpam on. Because spamSignals is a column, you can always reconstruct why a score landed where it did instead of trusting the number.
One signal was deliberately left out: a default-profile-picture heuristic, which returned zero hits on two independent live samples during the build and was judged not to generalise. It is not shipped and not scored.
The AI columns — model output, not TikTok data and not a fact
Four keys are written by a language model, and only when aiEnhancement is on. They are always present in the row; they are null unless a verdict actually landed.
| Field Name | Description |
|---|---|
aiIsSpam | The model's boolean opinion on whether the comment is spam — AI-derived inference about a person's post, not a verified fact. null when AI is off, unkeyed, or the call failed |
aiCategory | One of genuine, question, praise, criticism, joke, engagement_bait, promo_spam, scam, as returned by the model. The Actor does not validate the string against that list — whatever the model wrote is what you get |
aiSpamReason | The model's short justification, requested at a maximum of 12 words. Present in the row, not in the default table view |
aiError | Why no verdict exists: no API key supplied, model returned no verdict for this row, or an exception type and message truncated to 180 characters. null on a successful classification. Present in the row, not in the default table view |
aiIsSpam never removes a row. hideLikelySpam filters on the rule-based spamScore only, so the AI layer adds columns and changes nothing about what you are charged for.
Two honest limits on this layer. A picture-only comment is sent to the model as the literal placeholder [image or sticker comment, no text] — the model never sees the image, so any verdict on such a row is an opinion about a placeholder. And comments longer than 400 characters are truncated before being sent, so a long comment is judged on its opening.
Replies, and the run's own accounting
| Field Name | Description |
|---|---|
type | "comment" on every pushed row; "reply" on nested reply objects |
isReply | false on every pushed row; true on nested reply objects |
parentCid | null on a pushed row; the parent comment's cid on a nested reply |
replies | Array of nested reply objects, up to maxRepliesPerThread each. [] when the thread has none or replies are disabled |
scrapedAt | UTC ISO-8601 timestamp, identical on every row of one run |
Nested reply objects carry 46 keys — everything from the TikTok-published and media tables above, plus one key that exists only on replies, isCreatorReply, set from the reply endpoint's label_text marker. Replies do not carry the duplicate, spam, AI or scrapedAt fields: those are computed on top-level rows only.
Beyond the dataset, each run writes two files to the run's key-value store. output.json holds the full row array, and run-summary.json holds 22 accounting keys including rowsReturnedByApi, duplicateRowsRemoved, duplicateRowsCaughtWhilePaging, duplicateRowPercent, duplicateTextGroups, topDuplicateTexts, rowsWithMedia, mediaOnlyRows, rowsWithSpamSignal, rowsHighSpamRisk, collapsedRowsRemoved, rowsPushed, aiClassified and aiFailed — the audit trail for what the filters actually did.
🤖 Add-on: Need additional TikTok data?
A comment section answers "what did the audience say". TikTok Profile Scraper answers the other half — take a creator's post feed with per-post views, likes and captions, then feed its webVideoUrl values straight into this Actor's videoUrls. Tiktok Video Scraper (Creator Profile & Contact) starts from individual video URLs and returns the creator behind them with public contact details attached. For discovery before you pick videos, TikTok Trending Hashtags Scraper By Multiple Countries and Tiktok Trending Hashtags Analytics Scraper By Country surface what is rising.
Why not build this yourself?
TikTok publishes developer APIs, and neither serves an arbitrary third party's comment section on demand. The Display API returns data only for users who have completed an OAuth login with your own registered app, and the Research API is application-only with an eligibility review — check TikTok's current published developer documentation for the terms, which change and are not reproduced here. Reading a competitor's or an influencer's comments means reading the public endpoint, and that is the part worth understanding before you write it yourself.
The specific costs this Actor absorbs:
- HTTP 200 is not success on TikTok. A refused signed endpoint answers with a zero-byte 200. A WAF challenge answers with a small HTML page, also 200. This Actor never keys on status code alone: a 0-byte body is classified as
empty_body, and any body at or under 3,000 bytes that does not contain"comments"is classified aschallenge. Trust the status code and you will confidently report an empty comment section on a video with thousands of comments. - The comment endpoints are param-sensitive to the point of fragility. One extra query parameter on the comment list or reply list URL turns the response into that zero-byte 200. The request is built with exactly the parameters that work and nothing else.
- The API hands the same comment back on more than one page. Overlapping pagination is normal, not exceptional, and it grows with pagination depth. In a pay-per-result scraper every one of those repeats is a billed row. Getting it right means de-duplicating on
cidat two separate points — while paging, and again globally across every video in the run — before anything is pushed. - The reply endpoint has to be paged, not sliced. Fetching cursor 0 once and slicing the result caps replies at the page size no matter what the caller asked for. Real pagination means following the API's returned cursor, with a fallback for when it does not advance, and a page ceiling so a pathological thread cannot loop.
- Empty text is usually a picture.
image_listandcmt_sticker_structare nested several levels deep with multiple resolution fallbacks each. Skip that decoding and a large slice of a busy comment section reads as blank rows in your dataset. - Creator discovery needs a browser; comments do not. Resolving "the last 10 videos from a handle" means scrolling a profile page and intercepting its XHR responses, which is why headless Chromium is installed. The comment fetch itself is plain HTTP and never uses it.
- The proxy ladder. Direct works most of the time and costs nothing; residential is needed sometimes and costs money. Escalating in the right order, and only ever upward, is the difference between a cheap run and an expensive one.
How to use TikTok Comments Scraper By Real Comment Filter
The Actor runs on Apify. Start it from the Apify Console or call it through the Apify API — your Apify token is the only credential involved.
- Open the Actor on Apify and click Try for free
- Paste video links into TikTok video links (
videoUrls). Full URLs, shortvm.andvt.links and bare numeric video IDs all work. Or leave it empty and put handles into TikTok usernames (creatorUsernames) instead - Set Comments to keep per video (
maxCommentsPerVideo) and Replies to follow per comment (maxRepliesPerThread). Set replies to0to skip the reply endpoint entirely and run considerably faster - Optionally narrow the dataset: What to do with media comments (
mediaCommentHandling), Collapse identical comments into one row (collapseDuplicateTexts), and Hide rows that look like spam (hideLikelySpam) with its Spam score needed to hide a row (spamScoreThreshold) - Leave Ask an AI model to double-check each comment (
aiEnhancement) off unless you want the model columns and are willing to pay your own provider for them - Click Start, then export as JSON, CSV, Excel or XML, or read the dataset through the Apify API
No field is marked required in the schema, but one of videoUrls or creatorUsernames is required in practice. A run with both empty logs Provide at least one video URL or one creator username. and exits immediately — no rows pushed, nothing charged. A run whose creator discovery finds no videos logs No video URLs to process. Exiting. and does the same. The Console form ships with both fields pre-filled with example values, so pressing Start without editing anything scrapes the example video with 10 comments and 3 replies per thread, no key and no proxy needed.
If both fields are filled, the video links win and the usernames are ignored — including the pre-filled example username.
How to scrape comments from multiple TikTok videos in one run
videoUrls is a list, so bulk is the normal mode rather than a special case. Duplicate URLs in that list are removed before scraping, and up to 10 videos are processed concurrently with up to 10 reply threads in flight at a time. maxCommentsPerVideo applies per video — ten videos at 100 comments each is a 1,000-row budget before the filters take their cut.
Creator mode is the other bulk path: put handles in creatorUsernames, set videosPerCreator, and the Actor discovers each creator's video URLs first and then scrapes every one of their comment sections. Discovered URLs are de-duplicated across creators too, so a video reachable from two handles is scraped once.
What can you do with TikTok comment data?
- 🧹 A community manager triaging a viral video's comment section sorts on
spamScoreandspamSignals, answers theisTopCommentrows first, and usesduplicateTextCountto spot the copy-pasted wave without reading it forty times. - 🕵️ A brand-safety analyst vetting an influencer before a campaign reads
commenterCommentsInRun,isRepeatCommenterInRunandduplicateTextGroupIdacross a creator's last 20 videos to see whether the engagement comes from a broad audience or a handful of accounts repeating themselves. - 🖼️ A creative researcher studying how an audience actually responds sets
mediaCommentHandlingtoonlyMediaand pullsmediaUrls,stickerUrlandmediaTypeto build a picture-reply corpus that text-only scrapers throw away as blank rows. - 🌍 A regional social team filters on
commentLanguageand takestext,uniqueIdandcommentLinkfor the languages they staff, usingcommentLinkto jump straight back to the comment on TikTok to reply. - 📉 A product researcher tracking complaints exports
text,diggCountandcreateTimeISOfor every comment on a launch video and ranks by likes to find the criticism the audience itself upvoted. - 🤖 An AI engineer building a social-listening agent indexes
textwithcommentLanguage,spamRiskLevel,videoWebUrlandcreateTimeISOas retrieval metadata, filtering onspamScoreat query time so the agent answers from real comments rather than from a wall of engagement bait.
Every one of these is callable from an agent framework over the Apify API, since the Actor is a standard HTTP-triggered run returning typed JSON.
How does TikTok Comments Scraper By Real Comment Filter handle rate limits and blocking?
Three mechanisms, all automatic.
Block detection keyed on response size, not status code. TikTok answers a refused signed endpoint with HTTP 200 and zero bytes, and a WAF challenge with HTTP 200 and a small HTML page. Both are classified as blocks and logged as BLOCK SIGNAL lines: a 0-byte body becomes empty_body, and a body of 3,000 bytes or fewer that does not contain "comments" becomes challenge. A 200 that is neither, but is not parseable JSON, is logged as unparseable rather than treated as an empty result.
A proxy ladder that only ratchets upward. Every run starts on the container's direct connection with no proxy at all. On a block the Actor switches to an Apify datacenter proxy — using your proxyConfiguration if you enabled Apify Proxy, otherwise requesting one itself — and then to Apify residential with three fresh exit IPs, sticking at residential for the rest of the run once it gets there. Each switch is logged as a PROXY EVENT line. Across a fallback chain the Actor keeps whichever attempt returned the most rows.
Retries and pacing. Each request retries up to three times, with backoff on HTTP 403, 429 and 503 and on network errors and timeouts. Comment pages are fetched with a randomised 0.2–0.6 second pause between them and reply pages with 0.15–0.4 seconds, and pagination is bounded — up to 400 comment pages per video and 60 reply pages per thread. Comment requests time out at 30 seconds, reply requests at 15.
There is no CAPTCHA solving in this Actor and none is claimed. Headless Chromium is used only for creator discovery — resolving a username into video URLs — never for fetching comments. When a video still cannot be read after the whole ladder, that video simply contributes no rows: no error row is written for it, so check the run log, and compare videosProcessed against the row counts in run-summary.json.
⬇️ Input
Nineteen parameters, every one of them read directly from the Actor's input schema. Nothing is marked required — see the notes under the table for what that means in practice.
| Parameter | Required | Type | Description | Example Value |
|---|---|---|---|---|
videoUrls | No | array | TikTok video pages to pull the comment section from, one per line (stringList editor). Full links, short vm. and vt. links and bare numeric video IDs all work. Pre-filled with one example video URL. | ["https://www.tiktok.com/@mrbeast/video/7578547467189374239"] |
maxCommentsPerVideo | No | integer | How many unique comments to keep from each video. Duplicates are dropped before they count toward this number. Minimum 1, default 10. | 100 |
maxRepliesPerThread | No | integer | Replies pulled from each comment thread, nested under replies. Minimum 0, default 3. Set 0 to skip the reply endpoint entirely. | 3 |
deduplicateBy | No | string | "commentId" removes the same comment returned on two different pages. "commentIdAndText" additionally folds a commenter who posted word-for-word the same thing twice under two different IDs. Default "commentId". | "commentId" |
collapseDuplicateTexts | No | boolean | Emit one row per distinct comment text, keeping the most-liked example and reporting the group size in duplicateTextCount. Default false. | false |
mediaCommentHandling | No | string | "include" keeps everything, "onlyMedia" keeps only comments containing an image or sticker, "excludeMedia" drops them for a text-only dataset. Default "include". | "include" |
hideLikelySpam | No | boolean | Drops rows whose spamScore reaches spamScoreThreshold. Default false — the score is advisory, and leaving this off keeps every row with its score attached. | false |
spamScoreThreshold | No | integer | Score at which a row is hidden. Minimum 0, maximum 100, default 60. Only read when hideLikelySpam is on. | 60 |
creatorUsernames | No | array | TikTok usernames without the @, one per line (stringList editor). Used only when videoUrls is empty. Pre-filled with one example username. | ["mrbeast"] |
videosPerCreator | No | integer | Videos to collect per username before their comment sections are scraped. Minimum 1, maximum 1000000, default 10. | 10 |
creatorTabs | No | array | Which profile tab to read (select editor). Enum: videos (own uploads), reposts. At least one entry, no duplicates, default ["videos"]. | ["videos"] |
videoOrder | No | string | "latest" (newest first), "oldest", or "popular" (most liked). Default "latest". Oldest and popular are re-ordered locally from a wider candidate pool, so they cost more time. | "latest" |
videosPostedAfter | No | string | Keep only videos posted on or after this date (datepicker editor). Absolute YYYY-MM-DD or relative such as 3 months, 2 weeks, 10 days. No default. | "2026-01-31" |
videosPostedBefore | No | string | Keep only videos posted on or before this date (datepicker editor). Same formats. No default. | "2026-06-30" |
skipPinnedVideos | No | boolean | Ignore pinned videos, which sit at the top of a profile and are often months old. Default false. | false |
aiEnhancement | No | boolean | Adds aiIsSpam, aiCategory, aiSpamReason and aiError using your own provider key. One provider call per 25 comments, billed by your provider, not by Apify. Default false. | false |
aiModel | No | string | Model, and by prefix the provider: claude-haiku-4-5, claude-sonnet-5, claude-opus-4-8, claude-fable-5, gpt-4o-mini, gpt-4o, gpt-4.1-mini, gpt-4.1, o3-mini, o1, gemini-2.0-flash-lite, gemini-2.0-flash, gemini-1.5-pro, gemini-2.5-flash, gemini-2.5-pro, grok-2-latest, grok-3-mini, grok-3, grok-beta, deepseek-chat, deepseek-reasoner, sonar, sonar-pro, sonar-reasoning, mistral-small-latest, mistral-large-latest. Default "claude-haiku-4-5". | "claude-haiku-4-5" |
aiApiKey | No | string | Your own key for the provider implied by aiModel, stored encrypted as a secret field. Left empty, the matching provider environment variable is used instead. Read only when aiEnhancement is on. | "<YOUR_PROVIDER_API_KEY>" |
proxyConfiguration | No | object | Apify Proxy settings (proxy editor). Pre-filled with {"useApifyProxy": false} — a direct connection. Used as the datacenter rung of the fallback ladder if TikTok blocks the direct request. | {"useApifyProxy": false} |
⚠️ Ten undocumented input aliases exist, and they win over the parameters above. For compatibility with the base Actor's input shape, the Actor reads postURLs before videoUrls, profiles before creatorUsernames, commentsPerPost before maxCommentsPerVideo, maxRepliesPerComment before maxRepliesPerThread, resultsPerPage before videosPerCreator, profileScrapeSections before creatorTabs, profileSorting before videoOrder, oldestPostDateUnified before videosPostedAfter, newestPostDate before videosPostedBefore, and excludePinnedPosts before skipPinnedVideos. Base-key-first is deliberate — Apify pre-fills schema defaults into the run input, so reading the new key first would mean the base key was never reachable. The consequence for you: if you send both, the alias is what takes effect. Send one or the other, never both.
Further behaviours worth knowing, all read from the Actor's own input handling:
- Values are coerced, not rejected. An unrecognised
deduplicateByfalls back to"commentId"and an unrecognisedmediaCommentHandlingto"include", each with aCOERCED:warning in the log.spamScoreThresholdoutside0–100is clamped with the same warning. A non-numericmaxCommentsPerVideoormaxRepliesPerThreadfalls back rather than failing the run. - The schema default and the code's own fallback differ. Apify fills
maxCommentsPerVideowith the schema default of10andmaxRepliesPerThreadwith3, and that is what a Console run uses. If the key reaches the Actor genuinely absent, the code's internal fallbacks are100comments and0replies. Pass the values explicitly if you care which applies. videosPerCreator's maximum of 1,000,000 is not achievable. Discovery scrolls a profile page and stops after 80 scrolls, or after four consecutive scrolls that surface nothing new — whichever comes first. On top of that,videoOrderof"popular"or"oldest"sorts locally over a candidate pool explicitly capped at 200 videos, so those two orderings cannot see past the newest 200 posts however high you set this.- Date filters and
skipPinnedVideosapply to creator discovery only. They filter the videos a username resolves to. They have no effect at all when you supplyvideoUrlsdirectly, and no effect on comment timestamps. Contrary to what the field help implies, they are applied for all threevideoOrdervalues, including"popular". creatorTabsofrepostsis best-effort. Discovery loads the profile and clicks the Reposts tab within a 10-second window; many profiles do not have that tab at all, and when the click fails that tab silently contributes zero videos.proxyConfigurationgenuinely defaults to no proxy. The shipped prefill is{"useApifyProxy": false}and the run really does start on the container's direct IP. Your configuration is used for the datacenter rung of the ladder; if it cannot be built the Actor falls back to requesting an Apify proxy itself, and residential retries always use theRESIDENTIALgroup regardless of what you set.
Example input
{"videoUrls": ["https://www.tiktok.com/@mrbeast/video/7578547467189374239","https://vm.tiktok.com/ZMhKQ8pTa/","7521904337788128542"],"maxCommentsPerVideo": 200,"maxRepliesPerThread": 5,"deduplicateBy": "commentId","collapseDuplicateTexts": false,"mediaCommentHandling": "include","hideLikelySpam": false,"spamScoreThreshold": 60,"creatorUsernames": [],"videosPerCreator": 10,"creatorTabs": ["videos"],"videoOrder": "latest","videosPostedAfter": "2026-01-31","videosPostedBefore": "2026-06-30","skipPinnedVideos": false,"aiEnhancement": false,"aiModel": "claude-haiku-4-5","aiApiKey": "","proxyConfiguration": { "useApifyProxy": false }}
⬆️ Output
Typed, normalized JSON with a consistent shape across runs — one row per top-level comment, 65 top-level keys on every row, replies nested inside their parent. Unavailable values arrive as null or as an empty array rather than a missing key, including the four AI columns, which are present and null when aiEnhancement is off. Export as JSON, CSV, Excel or XML from the Console, or read the dataset through the Apify API. Three dataset views ship with the Actor: Clean comments (27 columns), Image & sticker comments (14) and Repeat text & spam signals (17).
Charging, stated plainly — and there is no filter expression to give you, because there is nothing to filter. Every row this Actor pushes is a real comment, pushed with the row_result charged event. There are no error rows, no accounting rows and no diagnostic rows: the codebase contains exactly one push call and it always charges. So there is no errorReason, no isError, no status and no marker key to exclude. If it is in the dataset, it is a comment and it was charged.
What that buys you is that the filters cost you nothing:
- Duplicates are removed before any push. The same
cidreturned on two pages is dropped while paging, and again globally across every video in the run before the first charge. A duplicate row is never written and never billed. - Filtered rows are never charged. Comments removed by
mediaCommentHandling, hidden byhideLikelySpam, or folded away bycollapseDuplicateTextsare dropped before the push loop. They cost you the fetch, not the row. - Filtered rows never reach your model either. The AI step runs last, after every filter, so a discarded comment burns no provider tokens.
- Replies are free. They are nested inside their parent row, not pushed as rows of their own, so a thread with 20 replies is still one charged row.
What it also means: a video that could not be read, that has no comments, or whose URL could not be parsed contributes nothing — no row, no marker, no charge. It is visible in the run log and in the videosProcessed figure in run-summary.json, not in the dataset.
Three limits to plan around, stated rather than glossed:
- Duplicate and spam counts are run-scoped, not video-scoped.
duplicateTextCount,duplicateTextGroupId,commenterCommentsInRunand thereforespamScoreare computed across every comment in the run. Scraping ten videos in one run gives different numbers than scraping them in ten runs. This is intentional — a bot posting the same line under ten videos is exactly what the run-wide view catches — but it means the score is not comparable across differently shaped runs. - De-duplication does not span runs. Comments are de-duplicated within a run only. Re-running the same video returns the same comments again, and charges again. De-duplicate on
cidon your side if you poll. - Replies carry no derived columns. Nested reply objects have the TikTok and media fields but no
spamScore, no duplicate grouping, no AI verdict and noscrapedAt. They are also de-duplicated only within their own thread.
Example output
One charged comment row, AI enhancement off:
{"videoWebUrl": "https://www.tiktok.com/@mrbeast/video/7578547467189374239","submittedVideoUrl": "https://www.tiktok.com/@mrbeast/video/7578547467189374239","input": "https://www.tiktok.com/@mrbeast/video/7578547467189374239","cid": "7578612904471692059","createTime": 1753362188,"createTimeISO": "2026-07-24T13:03:08.000Z","text": "the editing on this one is insane, how long did the timelapse take @jimmy","diggCount": 1842,"likedByAuthor": true,"pinnedByAuthor": false,"repliesToId": null,"replyCommentTotal": 27,"uid": "6904063862041396225","uniqueId": "milesonfilm","avatarThumbnail": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/5e2902b2db8018cbbde7a1aed077a35a~c5_100x100.jpg","mentions": ["jimmy"],"type": "comment","isReply": false,"parentCid": null,"videoTotalComments": 48211,"commentLanguage": "en","commentLink": "https://www.tiktok.com/@mrbeast/video/7578547467189374239?comment_id=7578612904471692059","secUid": "MS4wLjABAAAAqT7l0Zx3nB9r6mKQ2wYcVJdE1sHfXo4pR","commenterProfileUrl": "https://www.tiktok.com/@milesonfilm","nickname": "Miles","videoId": "7578547467189374239","isHighPurchaseIntentFlag": false,"replyScore": 0.8213,"showMoreScore": 0.4417,"foldStatus": -1,"noShow": false,"userBuried": false,"commentStatus": 1,"isTopComment": true,"isAuthorReply": false,"mediaUrls": [],"mediaThumbnails": [],"mediaDimensions": [],"mediaCount": 0,"stickerUrl": null,"stickerId": null,"hasMedia": false,"mediaType": null,"isMediaOnlyComment": false,"isEmptyWithoutMedia": false,"duplicateTextCount": 1,"isDuplicateText": false,"duplicateTextGroupId": null,"containsLink": false,"linkInText": null,"mentionCount": 1,"isMentionStuffed": false,"isEmojiOnlyText": false,"commenterCommentsInRun": 1,"isRepeatCommenterInRun": false,"spamSignals": [],"spamScore": 0,"spamRiskLevel": "none","isCollapsedDuplicateGroup": false,"aiIsSpam": null,"aiCategory": null,"aiSpamReason": null,"aiError": null,"scrapedAt": "2026-07-25T09:41:07.000Z","replies": [{"videoWebUrl": "https://www.tiktok.com/@mrbeast/video/7578547467189374239","cid": "7578620117762409498","createTimeISO": "2026-07-24T14:11:52.000Z","text": "40 days of shooting for 12 seconds of footage","diggCount": 903,"uniqueId": "mrbeast","nickname": "MrBeast","commenterProfileUrl": "https://www.tiktok.com/@mrbeast","type": "reply","isReply": true,"parentCid": "7578612904471692059","repliesToId": "7578612904471692059","isCreatorReply": true,"hasMedia": false,"mediaType": null,"isMediaOnlyComment": false}]}
A picture-only comment that a text-only scraper would return as blank, with a spam signal firing and AI enhancement on:
{"cid": "7578613440992215045","text": "","createTimeISO": "2026-07-24T13:09:44.000Z","diggCount": 3,"uniqueId": "dealsdailyshop27","nickname": "DAILY DEALS","commenterProfileUrl": "https://www.tiktok.com/@dealsdailyshop27","commentLanguage": null,"mediaUrls": ["https://p16-comment-sign-va.tiktokcdn.com/tos-maliva-avt-0068/oQBAfeIzTgNc4m~tplv-origin.image"],"mediaThumbnails": ["https://p16-comment-sign-va.tiktokcdn.com/tos-maliva-avt-0068/oQBAfeIzTgNc4m~tplv-crop.image"],"mediaDimensions": [{ "width": 1080, "height": 1080 }],"mediaCount": 1,"stickerUrl": null,"stickerId": null,"hasMedia": true,"mediaType": "image","isMediaOnlyComment": true,"isEmptyWithoutMedia": false,"duplicateTextCount": 0,"isDuplicateText": false,"duplicateTextGroupId": null,"containsLink": false,"linkInText": null,"mentionCount": 0,"isMentionStuffed": false,"isEmojiOnlyText": false,"commenterCommentsInRun": 4,"isRepeatCommenterInRun": true,"spamSignals": ["repeatCommenterInRun"],"spamScore": 15,"spamRiskLevel": "low","isCollapsedDuplicateGroup": false,"aiIsSpam": true,"aiCategory": "promo_spam","aiSpamReason": "image-only reply from an apparent shop account","aiError": null,"scrapedAt": "2026-07-25T09:41:07.000Z","replies": []}
Read that second row carefully. aiIsSpam is true while spamScore is only 15. The two layers disagree, the rule-based score is the one hideLikelySpam acts on, and the model was judging the placeholder [image or sticker comment, no text] rather than the picture itself. Neither is a verdict about the person.
How does it work?
Every video you supply is reduced to a numeric video ID — short vm. and vt. links are resolved by following the redirect, /video/ and /photo/ URLs are split, and a bare number of 15 digits or more is accepted as-is. The Actor then pages TikTok's public comment list endpoint with exactly the parameters that endpoint accepts and no others, asking for only as many comments as you still need rather than a fixed page, and following the cursor the API itself returns instead of guessing an offset. Threads with replies are paged separately against the reply endpoint. Creator mode adds a step in front: a headless Chromium session loads the profile, intercepts the feed XHR responses while scrolling, and resolves handles into video URLs.
Nothing is written twice. Comment IDs seen while paging are dropped immediately, and the whole run is de-duplicated again before the first row is pushed. Everything after that is pure computation on data already downloaded: media URLs decoded out of the nested payload, identical texts grouped and counted, five spam signals scored, your filters applied, and only then the optional model call.
Requests begin on a direct connection and escalate through Apify datacenter and residential proxies only when TikTok pushes back, with blocks detected by response size rather than status code. Only publicly visible comment data is returned — there is no login, no cookie field and no session anywhere in the input. Because the data comes from a JSON endpoint rather than CSS selectors, a TikTok front-end redesign does not change your field names.
Integrations
TikTok Comments Scraper By Real Comment Filter is an Apify Actor, so it works with anything that can call the Apify API or read a dataset.
Calling the Actor from Python
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("<YOUR_USERNAME>/tiktok-comments-scraper-by-real-comment-filter").call(run_input={"videoUrls": ["https://www.tiktok.com/@mrbeast/video/7578547467189374239"],"maxCommentsPerVideo": 200,"maxRepliesPerThread": 5,"hideLikelySpam": False,})for c in client.dataset(run["defaultDatasetId"]).iterate_items():print(c["spamScore"], c["spamRiskLevel"], c["uniqueId"], c["text"][:80])
Works in Go, Ruby, Node.js, cURL — any language that can make an HTTP request. Every row in the dataset is a comment, so no error-row branch is needed; filter on spamScore yourself if you kept hideLikelySpam off, which is the recommended way to run it.
No-code tools (n8n, Make, LangChain)
In n8n, use the Apify node — or an HTTP Request node pointed at the Actor's run endpoint with your token — and pass the same JSON input; an IF node on spamScore then splits the clean comments from the flagged ones into separate branches, and the flat rows drop straight into a Google Sheets or Postgres node. In Make, the Apify module supports run-and-wait, so a daily sweep of a campaign video's comment section can feed an Airtable or Slack step with no glue code. In LangChain, build documents from text with commentLanguage, spamRiskLevel, uniqueId and videoWebUrl as metadata — every value is already a typed primitive, array or plain object, so no output parser is needed. Apify schedules and webhooks cover recurrence and completion triggers.
Is it legal to scrape TikTok comments?
Scraping publicly visible pages is broadly treated as permissible where no authentication is bypassed and no private content is accessed, and this Actor does neither — it reads only what a signed-out visitor is served. But the output is unambiguously personal data, and this Actor goes one step further than most by attaching an automated judgement to each individual, so treat it that way from the first run.
uniqueId, nickname, uid, secUid, commenterProfileUrl and avatarThumbnail identify a real person. text is that person's own written expression, tied back to them by cid, commentLink, createTime and createTimeISO. mediaUrls, mediaThumbnails and stickerUrl are pictures they chose to post, mentions names third parties who never appeared in your input, and commentLanguage is an attribute of them. Every nested object inside replies carries the same fields about a different person. Under GDPR, UK GDPR and the CCPA, collecting and storing this is processing, and the fact that data is publicly available is not itself a lawful basis.
An inferred spam or bot label is itself personal data. spamScore, spamRiskLevel, spamSignals, isRepeatCommenterInRun and isMentionStuffed are the Actor's own inferences about a named individual, and aiIsSpam, aiCategory and aiSpamReason are a third-party model's. TikTok's isHighPurchaseIntentFlag is TikTok's inference, passed through. Inferred data attracts the same rights as observed data — including the right to rectification of an inference the person considers wrong, and the right to object to it. If you act on these labels rather than merely storing them, check whether your use engages the rules on automated decision-making before you build the workflow.
In practice that means six things:
- Lawful basis. Decide and document yours before you store rows. For research, brand safety and moderation that is usually legitimate interest, which requires a written balancing test weighing your purpose against the commenter's reasonable expectation that a throwaway comment would be aggregated, scored and retained in someone's database.
- Minimisation. If you only need the spam distribution and engagement volume, keep
spamScore,spamRiskLevel,diggCountandcreateTimeISO, and droptext,uniqueId,nickname,commenterProfileUrl,avatarThumbnail,mediaUrlsandmentionsfirst. SettingmediaCommentHandlingtoexcludeMediaalso keeps the pictures out of your dataset in the first place, which is a stronger position than collecting them and filtering later. - Transparency and retention. Set a retention period matched to your purpose and delete on schedule. If you are a controller in scope of the GDPR's transparency rules, Article 14 obliges you to inform people whose data you collected without contacting them first — and that obligation covers the inferred labels, not just the scraped text.
- Subject requests. Keep
cidanduidso a specific person's rows can be located and removed on an access, objection, rectification or erasure request. - AI enhancement adds a processor to your chain. With
aiEnhancementon, comment text — personal data written by an identifiable person — is sent to Anthropic, OpenAI, Google, xAI, DeepSeek, Perplexity or Mistral depending onaiModel. That provider processes it on your behalf under your own account, so it belongs in your record of processing activities, needs to be covered by your controller-processor terms with them, and should appear where your privacy notice lists recipients or international transfers. - Acting on the data is a separate regime from collecting it. Contacting commenters, or reporting and blocking them on the strength of a score, is governed by its own rules — marketing and cold-contact law plus TikTok's own terms if you act on-platform. A valid basis for collection does not carry over.
Consult legal counsel if your use case involves bulk storage of personal data, AI training corpora, resale, automated moderation decisions, or sending comment text to a model provider at scale.
❓ Frequently asked questions
What TikTok comment fields does this Actor return?
The five most used are text, uniqueId, diggCount, createTimeISO and spamScore. Every row also carries commentLanguage, commentLink, replyCommentTotal, the decoded media block (mediaUrls, stickerUrl, mediaType, isMediaOnlyComment), the duplicate block (isDuplicateText, duplicateTextCount, duplicateTextGroupId) and the full spam breakdown (spamSignals, spamRiskLevel and the five individual signal booleans). Sixty-five top-level keys in total — see the data fields section above.
Does it require a TikTok account, login or cookie?
No. There is no login field, no password field and no cookie field anywhere in the input schema. The Actor calls the same public comment endpoints a signed-out browser calls, and your Apify token is the only credential involved. One nuance for completeness: the creator-discovery browser will attach a sid_tt cookie if a TIKTOK_SID_TT environment variable happens to be set on the run, but nothing in the input schema sets one and comment fetching never uses it. The trade-off is that anything TikTok serves only to a logged-in session — comments on private or friends-only videos, age-gated content — is out of reach by design.
What does "real comment" mean here — is the filter an AI model?
No. The filter is a deterministic rule, and it works with AI switched off. spamScore is the weighted sum of five signals computed from the comment payload itself: a link in the text (45), the same text repeated elsewhere in this run (30), three or more mentions (30), text that is nothing but emoji (15), and the same commenter appearing three or more times in this run (15), capped at 100. hideLikelySpam compares that number to spamScoreThreshold and nothing else. The optional aiEnhancement layer adds aiIsSpam, aiCategory and aiSpamReason as extra columns — it never removes a row and never changes what you are charged. De-duplication and media decoding are likewise pure rules, not model output.
How accurate is the spam score and the AI label?
No accuracy figure is claimed for either, and you should not assume one. spamScore is a heuristic: it fires on patterns that correlate with spam, and a genuine comment containing a link, or a superfan posting under four videos in one run, will score too. aiIsSpam and aiCategory are a language model's opinion of a string truncated to 400 characters — and for a picture comment, of a placeholder rather than the picture. Both are inferences about a person's post, not facts about the person. That is why hideLikelySpam defaults to off, why every individual signal is exposed as its own column so you can audit the score instead of trusting it, and why the default threshold of 60 requires two independent signals rather than one. A signal that returned zero hits during the build — a default-profile-picture check — was deliberately not shipped rather than included to pad the score.
What happens if a video has no comments, is private, or was removed?
That video contributes no rows and no charges, and the run continues with the next one. There is no error row: this Actor pushes only real comments, so a video that returns nothing is visible in the run log rather than in the dataset. A URL the Actor cannot reduce to a video ID logs Could not extract a video ID from: <url>. A blocked or challenged response logs a BLOCK SIGNAL line naming whether it was a zero-byte refusal or a small challenge body, and triggers the proxy ladder before giving up. If every video in the run comes back empty, the Actor logs 0 comments collected. Nothing pushed, nothing charged. and still writes run-summary.json to the key-value store so the run stays auditable. Compare videosProcessed in that summary against your input count to find the gap.
How many comments can I extract, and can I scrape multiple videos at once?
Yes to multiple videos — videoUrls is a list, up to 10 videos are processed concurrently, and duplicate URLs are removed before scraping. maxCommentsPerVideo applies per video, has a minimum of 1 and no maximum in the schema, and is genuinely per-video: ten videos at 200 is a 2,000-row budget before filtering. The real ceilings are TikTok's and the Actor's pagination guards: comment pages are requested at up to 50 comments at a time and capped at 400 pages per video, reply pages at 20 at a time and capped at 60 pages per thread. maxRepliesPerThread is properly paginated rather than sliced from a single page, so values above the page size are genuinely delivered when the thread is that deep.
Am I charged for comments the filters removed, and does the AI option cost extra?
No to the first, yes to the second. Duplicate comment IDs, comments dropped by mediaCommentHandling, comments hidden by hideLikelySpam and rows folded away by collapseDuplicateTexts are all removed before the push loop, so they are never written and never billed — one row_result is charged per row that actually lands in your dataset, and nested replies are free. The AI option is billed by your own provider, not by Apify: one call per 25 comments, on your key, at their pricing. Because the model runs after every filter, you are never charged tokens for a comment the Actor then discards. And if aiEnhancement is on but no key is found in aiApiKey or the provider environment variable, the run does not fail — comments are still scraped, still written and still charged, with the AI columns null and aiError reading no API key supplied in the row itself.
How does this Actor compare to other TikTok comment scrapers?
All observations below were checked on the Apify Store on 25 July 2026 and describe what each listing documents — not measurements of our own.
clockworks/tiktok-comments-scraper is the most used in this category and the closest structural match: its documented input is postURLs, commentsPerPost and maxRepliesPerComment, and its published output sample shows nine keys — text, diggCount, replyCommentTotal, createTimeISO, uniqueId, videoWebUrl, uid, cid, avatarThumbnail — every one of which this Actor also emits under the same name, so migrating either way needs no remapping. It documents error items identified by an errorCode field with a 17-code reference table, and its README is silent on whether those items are billed. It states pay-per-result pricing of "$5 to scrape 1,000 results". Its listing does not mention de-duplication, image or sticker decoding, duplicate-text grouping or spam scoring — where a listing is silent, that is not documented rather than not supported.
apidojo/tiktok-comments-scraper documents startUrls, includeReplies, maxItems and a customMapFunction transform hook, and its output sample uses a different key layout throughout — parentId, id, text, createdAt, likeCount, replyCount, commentLanguage, awemeId, isAuthorLiked and a nested user object carrying region, bio, hasEmail and hasPhone — so migrating to or from it does mean remapping. Its listing also asks that each input URL fetch at least 10 comments, and states "No Proxy Required". The speed, success-rate and price figures on that listing ("10,000 comments per minute", "100-200 comments per second", "99% success rate", "$0.30 per 1,000 comments") are its own claims and are neither reproduced as fact nor endorsed here.
epctex/tiktok-comment-scraper documents startUrls as required plus includeReplies, endPage, maxItems and customMapFunction, and returns TikTok's raw snake_case payload shape — cid, text, create_time, digg_count, reply_comment_total, comment_language, aweme_id, is_author_digged, reply_id and a nested user object. Its listing prices three separate events: "$0.003 per video" for a comment query, "$0.003 per comment thread" for a reply query, and "$0.0003 per item" for dataset items beyond a stated free allowance.
Observable differences here: comment IDs de-duplicated at two points before anything is pushed or billed; image and sticker comments decoded into mediaUrls and stickerUrl instead of arriving as blank text; repeated texts grouped with a count and an optional collapse; a five-signal spam score with every signal exposed as its own auditable column; a filter chain in which excluded rows are never charged; and an optional model layer on your own key across seven providers, kept strictly additive. What it does not do: no commenter region, bio, email or phone flags, no customMapFunction hook, no error rows, and no de-duplication across runs.
Does it work with Claude, ChatGPT and other AI agent tools, and is the output LLM-ready?
Yes to both. The Actor is callable as a standard HTTP-triggered Apify run, so LangChain, CrewAI, n8n or a hand-written tool definition can start it and read back typed, normalized JSON with consistent field names across runs — no HTML parsing, no selectors, no relative-date strings to resolve. There is no separate signup and no separate API key for the run itself: the Apify token you already have is the only credential. Note that this is a different question from aiEnhancement, which is about the Actor calling a model rather than a model calling the Actor.
What happens when TikTok changes its layout or anti-bot system, and can I run it without managing proxies?
The Actor is maintained, the output schema stays stable on your side, and there is nothing for you to configure. Two design choices reduce the exposure: comments come from a JSON endpoint rather than rendered markup, so a front-end redesign does not move your field names, and block detection keys on response size and body content rather than HTTP status, which is what actually changes when TikTok's WAF adjusts. Anti-bot pushback is handled as escalation rather than failure — retry with backoff, then a datacenter proxy, then residential with fresh exit IPs. There is no CAPTCHA solving and none is claimed. You never install a browser, create a proxy account or rotate an IP yourself; the only browser in the Actor is the headless Chromium used to resolve creator usernames into video URLs, and it is skipped entirely when you supply videoUrls.
Which TikTok comment fields work best for AI training data and RAG indexing?
For RAG indexing, text carries the entire information payload of a comment and chunks cleanly one row per chunk, with commentLanguage, spamRiskLevel and mediaType as filter metadata and commentLink or videoWebUrl as the citation anchor. Filter on spamScore at index time rather than at scrape time so you can retune the cut-off without re-scraping. For training data and analytics, the most structurally consistent fields across records are diggCount, replyCommentTotal, createTime, spamScore, duplicateTextCount, mentionCount and commenterCommentsInRun — all typed primitives written on every row. Three things to presence-check first: text is "" rather than null on picture comments, so check isMediaOnlyComment before treating an empty string as a bad record; commentLanguage is genuinely null when TikTok did not label the comment; and the four AI columns are null on every row unless aiEnhancement was on, so exclude them from any schema that assumes a value.
🔗 Related scrapers
| Scraper Name | What it extracts |
|---|---|
| TikTok Profile Scraper | A creator's post feed with views, likes, captions and video URLs — the input side of this Actor |
| Tiktok Video Scraper (Creator Profile & Contact) | Single-video data with the creator's profile and public contact details attached |
| TikTok Trending Hashtags Scraper By Multiple Countries | Rising TikTok hashtags across several countries in one run |
| Instagram Comment Leads Scraper | Instagram comment threads with commenter identity attached |
| LinkedIn Comment Scraper by Language | LinkedIn post comments with a per-comment language and script verdict |
| Pinterest Comment Scraper With Commenter Profile Enrichment | Pinterest pin comments with the commenter's profile resolved |
Which one suits which job: use TikTok Profile Scraper when you need the posts, then hand its webVideoUrl values to this Actor when the audience reaction matters more than the post metrics.
💬 Your feedback
Found a bug, a spam signal that keeps misfiring on real comments, or a field in TikTok's comment payload that is not in this output? Open an issue on the Actor's Issues tab. Reports that include the exact input JSON, the video URL and the cid of the comment involved are the fastest to reproduce and fix.