# TikTok Comments Scraper - Comments, Authors & Likes (`abotapi/tiktok-comments-scraper`) Actor

Scrape comments from TikTok videos using one or more video URLs or IDs. Extract comment text, author, likes, reply count and timestamp in clean structured data. Supports multiple videos in a single run with no login required.

- **URL**: https://apify.com/abotapi/tiktok-comments-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## TikTok Comments Scraper

Collect the comments on TikTok videos — either the exact videos you paste, or by giving usernames and letting the actor walk their recent videos. Every run can filter, resume, track changes across scheduled runs, and pipe results into your own apps.

### Why this scraper

- **Two ways to pick videos.** Paste video links/ids, or give usernames and let the actor discover each account's recent videos.
- **Filters that save money.** Drop low-like comments, require a word, exclude replies, or keep only recent comments — filtered rows are never returned and never billed.
- **Built to survive a flaky source.** The mobile endpoint answers only a share of requests and only during certain windows, so each page is sampled patiently until it succeeds rather than given up on.
- **Recurring monitoring.** Incremental mode labels every comment NEW / UPDATED / UNCHANGED / REAPPEARED / EXPIRED across runs, so a daily schedule returns only what changed.
- **Resume big crawls.** Paste a previous run id and already-collected comments are skipped.
- **Export to your apps.** Optionally pipe results into Notion, Linear, Airtable or Apify through MCP connectors.
- An empty result is never reported as a plain success, so a scheduled run cannot fail quietly.

### Data you get

> Sample shape: values are illustrative placeholders, not from a live video.

| Field | Example |
| --- | --- |
| recordType | "comment" |
| sourceMode | "comments" |
| commentId | "7000000000000000000" |
| videoId | "7000000000000000001" |
| videoUrl | "https://www.tiktok.com/video/7000000000000000001" |
| isReply | false |
| replyToCommentId | null |
| text | "Sample comment text" |
| authorUsername | "sampleuser" |
| authorNickname | "Sample User" |
| authorUid | "7000000000000000002" |
| authorAvatarUrl | "https://p16-sign-va.tiktokcdn.com/sample-avatar.jpeg" |
| authorRegion | "US" |
| diggCount | 12 |
| replyCommentTotal | 3 |
| createTime | 1700000000 |
| createTimeIso | "2026-01-01T00:00:00.000Z" |
| scrapedAt | "2026-01-01T00:00:00.000Z" |
| changeType | "NEW" (incremental mode only) |

### How to use

Videos mode — the exact videos you care about:

```json
{
  "mode": "videos",
  "videoUrls": ["https://www.tiktok.com/@tiktok/video/7673909736131038495"],
  "maxCommentsPerVideo": 50
}
```

Search mode — walk an account's recent videos:

```json
{
  "mode": "search",
  "usernames": ["tiktok"],
  "maxVideosPerUser": 10,
  "maxCommentsPerVideo": 50
}
```

Recurring monitoring — only what changed since yesterday:

```json
{
  "mode": "videos",
  "videoUrls": ["https://www.tiktok.com/@tiktok/video/7673909736131038495"],
  "incrementalMode": true,
  "stateKey": "my-campaign"
}
```

### Input parameters

#### Start

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| mode | string | videos | videos = scrape the URLs/ids you paste; search = walk usernames' videos. |

#### Videos mode

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| videoUrls | array of strings | \[] | Video URLs or numeric video ids. Short share links (vm.tiktok.com) are not supported. |

#### Search mode

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| usernames | array of strings | \[] | TikTok usernames (with or without @). |
| maxVideosPerUser | integer | 10 | How many of each account's recent videos to scan. 0 = unlimited. |

#### Filters (both modes)

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| minCommentLikes | integer | 0 | Only keep comments with at least this many likes. 0 = no minimum. |
| commentKeyword | string | - | Only keep comments containing this word or phrase (case-insensitive). |
| excludeReplies | boolean | false | Drop replies and keep only top-level comments. |
| commentsSinceDays | integer | 0 | Drop comments older than this many days. 0 = no date filter. |
| fetchReplies | boolean | false | Also try to collect replies. The source currently returns no reply rows to automated sessions. |
| maxRepliesPerComment | integer | 5 | Reply cap per comment. 0 = unlimited. |
| replyPatienceSeconds | integer | 10 | Time budget per reply request. |

#### Output & limits

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| maxCommentsPerVideo | integer | 20 | Comments per video. 0 = unlimited. |
| maxPagesPerVideo | integer | 0 | Safety cap on result pages per video. 0 = no page limit. |
| pagePatienceSeconds | integer | 120 | Per-page time budget; the page keeps being requested until it succeeds or this runs out. |
| maxAttemptsPerPage | integer | 0 | Optional hard ceiling on requests per page. 0 = no ceiling. |
| concurrency | integer | 20 | Parallel requests (1-20). |

#### Resume and recurring updates

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| resumeFromRunId | string | - | Previous run or dataset id; comments already collected there are skipped. |
| incrementalMode | boolean | false | Label each comment NEW / UPDATED / UNCHANGED / REAPPEARED / EXPIRED against the remembered baseline and suppress unchanged rows. |
| stateKey | string | - | Optional name for this monitoring campaign. Empty = derived from the videos and filters. |
| emitUnchanged | boolean | false | Also return comments that did not change. |
| emitExpired | boolean | false | Also return comments that are gone. Only after a run that finished the whole tracked scope. |

#### Export and connection

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| mcpConnectors | array | \[] | Optional MCP connectors to also send results to. Never changes the dataset. |
| notionParentPageUrl | string | - | Notion parent page for the Notion connector. |
| maxNotifyListings | integer | 50 | Cap on items sent to each connector per run. |
| proxyTier | string | datacenter | datacenter (default, cheapest) or residential. |
| proxy | object | Apify proxy | Proxy configuration. |

### About request attempts

The source deliberately answers only a share of requests from automated clients and refuses the rest, and it does so in windows (some minutes let almost nothing through). That refusal is per request, not per session, so the actor fires many requests for the same page in parallel and takes the first one that gets through. `concurrency` is how many run at once, and `pagePatienceSeconds` is how long the actor keeps sampling before giving up on a page — so a window that opens partway through is still caught.

A refused request is tiny, and only one winning response is transferred per page, so a run stays cheap even while it samples many times. If a run reports a video as "could not be read", the source was closing every request during that window; raising `pagePatienceSeconds` (for example to 240) gives it more time to open.

### How a run reports its result

An empty dataset is never reported as a plain success.

- **Failed run (red).** No comments collected and at least one video could not be read.
- **Partial run (green, warning).** Some comments collected but at least one video could not be read.
- **Empty result (green).** Every video was read and had no matching comments.
- **Clean run (green).** Every video was read and returned comments.

### Known limitations

- **Replies are usually not returned by the source.** Inline reply previews carried by the comment list are captured; full reply threads are not offered while the source withholds them. Each attempt still costs a "Reply fetch attempted" charge whether or not it returns anything (the actor still pays for the extra request); any reply rows that do come back are billed separately as ordinary Results.
- **Keyword/hashtag video search is not supported.** Search mode walks usernames' videos; general TikTok keyword search needs a browser-rendered page, which would make runs far more expensive.
- **Short share links** (vm.tiktok.com, vt.tiktok.com) are not resolved; paste the full video URL or the numeric video id.
- **Only public videos.** A private or removed video returns no comments.
- The comment list the source returns can differ slightly between requests, so incremental runs may legitimately report a few NEW comments each time.

# Actor input Schema

## `mode` (type: `string`):

How to choose the videos. 'videos': scrape the video URLs/ids you paste. 'search': give usernames and the actor walks their recent videos, then scrapes comments on those.

## `videoUrls` (type: `array`):

One or more TikTok videos to pull comments from. Paste the full video URL (https://www.tiktok.com/@user/video/1234567890) or just the numeric video id. Short share links (vm.tiktok.com) are not supported.

## `usernames` (type: `array`):

One or more TikTok usernames (with or without @). The actor lists each account's recent videos and scrapes their comments.

## `maxVideosPerUser` (type: `integer`):

How many of each account's most recent videos to scan for comments. 0 = unlimited.

## `minCommentLikes` (type: `integer`):

Only keep comments with at least this many likes. 0 = no minimum.

## `commentKeyword` (type: `string`):

Only keep comments whose text contains this word or phrase (case-insensitive). Leave empty for no text filter.

## `excludeReplies` (type: `boolean`):

Keep only top-level comments and drop replies to other comments.

## `commentsSinceDays` (type: `integer`):

Drop comments older than this many days. 0 = no date filter.

## `fetchReplies` (type: `boolean`):

Also try to collect replies. The source currently returns no reply rows for automated sessions, so this usually adds nothing; leave it off unless you want to try.

## `maxRepliesPerComment` (type: `integer`):

Cap on replies collected per comment when reply fetching is enabled. 0 = unlimited.

## `replyPatienceSeconds` (type: `integer`):

Time budget for each reply request. Only used when reply fetching is enabled.

## `maxCommentsPerVideo` (type: `integer`):

How many comments to collect from each video. 0 = unlimited.

## `maxPagesPerVideo` (type: `integer`):

Safety cap on result pages per video. 0 = no page limit (the comment cap above still applies).

## `pagePatienceSeconds` (type: `integer`):

The source answers only a share of requests, and only during certain windows (sometimes shut for a minute or more), so each page keeps being requested until one succeeds or this budget runs out. Raise it for stubborn videos.

## `maxAttemptsPerPage` (type: `integer`):

Optional hard ceiling on requests per page. 0 = no ceiling (the seconds budget above is the only limit).

## `concurrency` (type: `integer`):

How many requests run at the same time. Higher samples more exits per second. 20 is the maximum.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID or dataset ID to continue a large crawl without re-collecting or re-billing comments already saved there.

## `incrementalMode` (type: `boolean`):

Turn this on for recurring monitoring: each comment is labelled NEW, UPDATED, UNCHANGED or REAPPEARED against the remembered baseline, and unchanged comments are not returned. Turn on Emit unchanged or Emit expired only if you also want those rows returned (and billed).

## `stateKey` (type: `string`):

Optional name for this monitoring campaign. Leave empty to let the actor derive a key from the videos and filters.

## `emitUnchanged` (type: `boolean`):

Return comments that did not change since the last run. Off by default because unchanged rows are usually noise.

## `emitExpired` (type: `boolean`):

Return comments that were in the baseline but are gone this run. Only emitted after a run that provably finished the whole tracked scope.

## `mcpConnectors` (type: `array`):

Optionally send results into the apps you already use, via Model Context Protocol connectors. Authorize one under Apify, Settings, API & Integrations, then select it here. Notion gets a rich page-per-item export; other connectors get a best-effort write or digest. Leave empty to skip; never changes the dataset output.

## `notionParentPageUrl` (type: `string`):

URL or id of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## `proxyTier` (type: `string`):

Datacenter is the default and is enough for this source. Residential is available if datacenter runs return too few comments.

## `proxy` (type: `object`):

Leave apifyProxyGroups unset so the 'Proxy tier' field above decides. Set it here only to pin a non-standard group. Leave the country empty; exits rotate automatically.

## Actor input object example

```json
{
  "mode": "videos",
  "videoUrls": [
    "https://www.tiktok.com/@tiktok/video/7673909736131038495"
  ],
  "usernames": [
    "tiktok"
  ],
  "maxVideosPerUser": 10,
  "minCommentLikes": 0,
  "excludeReplies": false,
  "commentsSinceDays": 0,
  "fetchReplies": false,
  "maxRepliesPerComment": 5,
  "replyPatienceSeconds": 10,
  "maxCommentsPerVideo": 20,
  "maxPagesPerVideo": 0,
  "pagePatienceSeconds": 120,
  "maxAttemptsPerPage": 0,
  "concurrency": 20,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "maxNotifyListings": 50,
  "proxyTier": "datacenter",
  "proxy": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "mode": "videos",
    "videoUrls": [
        "https://www.tiktok.com/@tiktok/video/7673909736131038495"
    ],
    "usernames": [
        "tiktok"
    ],
    "maxVideosPerUser": 10,
    "minCommentLikes": 0,
    "commentsSinceDays": 0,
    "maxRepliesPerComment": 5,
    "replyPatienceSeconds": 10,
    "maxCommentsPerVideo": 20,
    "maxPagesPerVideo": 0,
    "pagePatienceSeconds": 120,
    "maxAttemptsPerPage": 0,
    "concurrency": 20,
    "maxNotifyListings": 50,
    "proxy": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/tiktok-comments-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "mode": "videos",
    "videoUrls": ["https://www.tiktok.com/@tiktok/video/7673909736131038495"],
    "usernames": ["tiktok"],
    "maxVideosPerUser": 10,
    "minCommentLikes": 0,
    "commentsSinceDays": 0,
    "maxRepliesPerComment": 5,
    "replyPatienceSeconds": 10,
    "maxCommentsPerVideo": 20,
    "maxPagesPerVideo": 0,
    "pagePatienceSeconds": 120,
    "maxAttemptsPerPage": 0,
    "concurrency": 20,
    "maxNotifyListings": 50,
    "proxy": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/tiktok-comments-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "mode": "videos",
  "videoUrls": [
    "https://www.tiktok.com/@tiktok/video/7673909736131038495"
  ],
  "usernames": [
    "tiktok"
  ],
  "maxVideosPerUser": 10,
  "minCommentLikes": 0,
  "commentsSinceDays": 0,
  "maxRepliesPerComment": 5,
  "replyPatienceSeconds": 10,
  "maxCommentsPerVideo": 20,
  "maxPagesPerVideo": 0,
  "pagePatienceSeconds": 120,
  "maxAttemptsPerPage": 0,
  "concurrency": 20,
  "maxNotifyListings": 50,
  "proxy": {
    "useApifyProxy": true
  }
}' |
apify call abotapi/tiktok-comments-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/tiktok-comments-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/WQXUYI3DXPQbn0yf7/builds/64MCnfvKSOvRYwhgU/openapi.json
