# TikTok Scraper (`dami_studio/tiktok-scraper`) Actor

TikTok scraper API for videos, profiles and hashtags, by handle, hashtag, keyword or link. Video rows carry the TikTok analytics: plays, likes, comments, shares, saves, plus caption, sound and author. A profile gives its own row and its newest 10 or so videos. $0.25 per 1,000 rows.

- **URL**: https://apify.com/dami_studio/tiktok-scraper.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** Social media
- **Features:** Limited permissions
- **Stats:** 130 total users, 71 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.24 / 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 scraper for TikTok videos and profiles by handle, hashtag, keyword or link**: every video comes back
with its plays, likes, comments, shares and saves, the caption, the sound, the author and the file
links, and every profile with followers, lifetime likes, bio and the verified flag. Nothing to log
into.

One thing to know before you spend anything: a profile returns about a dozen rows, the profile
itself plus its ten or eleven newest videos. No value of `resultsPerPage` changes that. Hashtags and keyword searches page much deeper, so those
are the modes to use when you want volume.

| | |
|---|---|
| **Input** | Handles, hashtags, keywords, or TikTok profile, video and tag links |
| **Output** | One row per video or profile: plays, likes, comments, shares, saves, caption, hashtags, tagged accounts, sound, author, file links and the input it came from; followers, lifetime likes and bio on profiles |
| **Ceiling** | 2,000 rows per run, or per input with `resultsPerInput`; a profile gives about 12 |
| **Account needed** | None |
| **Price** | $0.25 per 1,000 rows on the free plan, down to $0.244 on Business. The free plan's $5 a month covers about 20,000 |

| Using an AI agent? | |
|---|---|
| Actor | `dami_studio/tiktok-scraper` |
| Set one field | `hashtags`, labelled "Hashtags" in the Console |
| Cap the bill | `maxTotalChargeUsd` on the run, for example `0.10`: it never charges past that |
| MCP, one link | `https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/tiktok-scraper` |
| How often | Daily, to follow a hashtag or an account over time |

### 🔍 What TikTok Scraper does

Four ways in, and you can mix them in one run:

| You give it | You get |
|---|---|
| `profiles` | The profile row, then its newest videos, about a dozen rows per handle |
| `hashtags` | Videos posted under the tag, paging as deep as your limit allows |
| `searchQueries` | The videos TikTok returns for the keyword |
| `startUrls` | A profile link, a `/video/` link for one video, or a `/tag/` link |

`resultsPerPage` counts rows for the **whole run**, not per input. That matters when you list
several: once the first hashtag has used the allowance, the second returns nothing. Set
`resultsPerInput` and each input gets its own limit.

Rows are deduplicated inside a run, so a video that turns up under two of your hashtags is returned
and billed once. `maxRunSeconds` stops a slow feed and hands back what it has so far, which keeps a
search that crawls along from running up a bill.

### 📋 What data you get from each TikTok video and profile

| What you get | Field |
|---|---|
| Which kind of row it is | `type` (`video` or `profile`) |
| Video id and the link a person can open | `id`, `webVideoUrl` |
| Caption, and the hashtags in it | `text`, `hashtags` |
| Accounts tagged in the caption | `mentions` |
| Which of your inputs found it | `sourceInput`, `sourceType` |
| TikTok's own subtitles, when you ask for them | `subtitles`, `subtitlesLanguage` |
| When it went up, and the region TikTok files it under | `createTime`, `region` |
| Plays, likes, comments, shares, saves and downloads | `playCount`, `diggCount`, `commentCount`, `shareCount`, `collectCount`, `downloadCount` |
| Whether it is an ad | `isAd` |
| Length, cover image and the video file links | `video.duration`, `video.cover`, `video.dynamicCover`, `video.playAddr`, `video.downloadAddr` |
| The sound: id, title, artist, audio link, original or not, length | `music` |
| Who posted it | `author.id`, `author.uniqueId`, `author.nickname`, `author.avatar` |
| Profile handle, display name, bio, avatar and the blue tick | `uniqueId`, `nickname`, `signature`, `avatar`, `verified` |
| Followers, following, lifetime likes and video count | `followerCount`, `followingCount`, `heartCount`, `videoCount` |
| The profile id that survives a rename, and the profile link | `secUid`, `url` |

### ▶️ How to scrape TikTok videos and profiles

1. Open [TikTok Scraper](https://apify.com/dami_studio/tiktok-scraper) and click **Try for free**.
2. Fill in whichever box fits: **Profiles**, **Hashtags**, **Search queries** or **Start URLs**.
3. Set **Max results per run**, or **Results per input** to give each input its own limit. Start
   at 50 while you look at the output.
4. Click **Start**.
5. Download the dataset as JSON, CSV or Excel, or pull it from the Apify API.

Start it with every box empty and you get one labelled sample row that is not charged. It is a
placeholder, so judge the real shape from the row further down this page.

### 💰 How much does it cost to scrape TikTok videos and profiles?

**$0.25 per 1,000 rows.** Paid plans pay a little less: $0.248 on Starter, $0.246 on Scale and $0.244 on Business. A profile row costs the
same as a video row. On the free plan, the $5 Apify gives you each month covers about 20,000 rows.

You pay for rows that carry data. The sample row, diagnostic rows and duplicates caught inside a
run are not charged, and `resultsPerPage` caps both the rows and the spend.

Leaving out pinned videos is a paid option, billed per video left out as "Pinned video left out"
(see the Pricing tab). From 16 November 2026 the likes and date filters work the same way: $0.01
per 1,000 videos they leave out, billed as "Video left out by your filters", and only when you set
them. A video whose likes or date could not be read is left out free. Subtitles cost nothing extra.

One case people miss: a private account still costs its profile row, because the profile is real
data even when the videos behind it are hidden.

### 📥 What you give it

```json
{
  "profiles": ["nasa"],
  "hashtags": ["spacex"],
  "resultsPerPage": 50,
  "maxRunSeconds": 90
}
```

| Field | Default | What it is |
|---|---|---|
| `profiles` | none | Handles, with or without `@`. The form opens with `nasa` filled in as an example, which is a starting point rather than a default. |
| `hashtags` | none | Tags, with or without `#`. |
| `searchQueries` | none | Keywords to search TikTok for. |
| `startUrls` | none | Links pasted straight in: a profile, a `/video/` link or a `/tag/` link. Anything else is skipped. |
| `resultsPerPage` | `50` | Rows for the **whole run**, 1 to 2,000. This is also your spend cap. Not used when `resultsPerInput` is set. |
| `resultsPerInput` | none | Rows for **each** profile, hashtag, search and link, 1 to 2,000. Empty keeps the one limit for the run. |
| `maxRunSeconds` | `90` | Stops the scrape at this point and returns what it has, 10 to 300. Raise it if you would rather have the rows than the time back. |
| `minLikes`, `maxLikes` | none | Optional add-on, off unless you set a value: keep only videos inside this likes range. A video with no like count is left out, the same as one outside the range; a count of 0 is a real count. These filters apply to videos only: a profile's own row is always returned, and its `diggCount` is the number of videos that account has liked. |
| `postedAfter`, `postedBefore` | none | Optional add-on, off unless you set a date: keep only videos posted on or after `postedAfter` and before `postedBefore`, as dates like `2026-10-01`, counted in UTC. A video with no posting date is left out, the same as one outside the range. Anything that is not a date stops the run before it starts, with a `BAD_INPUT` row. With the likes or date add-on set, a hashtag or search input stops after 10 pages in a row with no match. |
| `excludePinned` | `false` | Paid option, profiles only: leave out the videos a creator pinned to the top of their profile. A pinned video that is also the creator's newest stays in. |
| `includeSubtitles` | `false` | Add TikTok's own subtitles to each video row, as text. Hashtag and search runs take longer with it on. |
| `sessionCookies` | none | Optional: your own session, used only for your run. You are responsible for using it within the site's terms. The field is stored encrypted. |
| `proxyConfiguration` | off | Leave it off. It is there for people sending traffic through their own servers at high volume. |
| `notionConnector`, `notionParentId` | none | Optional. Write each row into a Notion database as well as the dataset. |

### 📤 What you get back

![Real run output](https://api.apify.com/v2/key-value-stores/BTBHU3DdiuoF4JOsK/records/tiktok-scraper-output.png)

A real video row from a real run. The caption ends with an emoji, so it is cut short here, and the
media links are trimmed:

```json
{
  "ok": true,
  "type": "video",
  "id": "7358577498319342891",
  "webVideoUrl": "https://www.tiktok.com/@.../video/7358577498319342891",
  "text": "Got me feeling like Snow White and shii... #fyp #cats #disneyprincess",
  "createTime": "2024-04-16T21:19:04.000Z",
  "hashtags": ["fyp", "cats", "disneyprincess"],
  "region": "US",
  "playCount": 17912733,
  "diggCount": 3674504,
  "commentCount": 12854,
  "shareCount": 244131,
  "collectCount": 282047,
  "downloadCount": 8092,
  "isAd": false,
  "video": { "duration": 11, "cover": "https://p16-common-sign.tiktokcdn-us.com/...", "playAddr": "https://v45.tiktokcdn-us.com/...", "downloadAddr": "https://v45.tiktokcdn-us.com/..." },
  "music": { "id": "7402610556690860833", "title": "kagefumi", "authorName": "-", "original": true, "duration": 36 },
  "author": { "id": "6806757644620350470", "uniqueId": "...", "nickname": "...", "avatar": "https://p19-common-sign.tiktokcdn-us.com/..." }
}
```

| Field | How to read it |
|---|---|
| `type` | Videos and profiles share one dataset, so check this before anything else. |
| `playCount` and the other counters | Read at the moment of the run, `null` when TikTok gives no figure. |
| `text`, `hashtags` | Empty when the creator wrote no caption or used no tags. |
| `video.playAddr`, `video.downloadAddr`, `video.cover` | Signed links that stop working after a short while. |
| `music` | The sound the video uses, or `null` when there is none. |
| `mentions` | The accounts tagged in the caption, each as `{ "uniqueId", "id" }`. The caption shows a tag as the person's display name, so read the handle here. On hashtag and search rows only the `id` is known and `uniqueId` is `null`; on some of a profile's videos, `mentions` is `null`. |
| `sourceInput`, `sourceType` | What you typed that found the row, and whether it was a `profile`, `hashtag`, `search` or `videoUrl`. A video found by two inputs comes once, under the first. |
| `subtitles`, `subtitlesLanguage` | Only with `includeSubtitles` on: the subtitles TikTok publishes for the video as plain text, and their language, such as `eng-US`. Both `null` when TikTok has none for that video or the file could not be read. |
| `secUid` | On profile rows. It stays the same when the creator changes their handle, so track it rather than `uniqueId`. |

### 🧾 Reading the output

Three kinds of row can land in your dataset.

| Row | How to spot it | Billed |
|---|---|---|
| A video or a profile | `ok: true` and a `type` | yes |
| The sample row | `_sample: true` | no |
| A diagnostic | `ok: false` and an `errorCode` | no |

Filter on `ok == true` and you have your data. A diagnostic never stops the run; it moves on to
the next input.

| Code | What it means |
|---|---|
| `NOT_FOUND` | TikTok shows the handle, video or tag as gone or private. |
| `BAD_INPUT` | A handle carried characters TikTok does not use, or an option could not be used, such as a date that is not a date. For an option, nothing is searched. |
|`RATE_LIMITED`, `SERVER_ERROR`, `NETWORK` | That item would not answer this run. Re-running usually clears it. |
| `NOT_SERVED` | This request was not served this run. Re-run it later. Runs before 13 October 2026 wrote `BLOCKED` here. |
| `TIME_LIMIT` | The run reached `maxRunSeconds` or the time limit set for it on Apify and stopped, keeping every row it had delivered. If nothing had come back by then, the run still ends as succeeded, with an amber status message. |
| `COST_LIMIT` | The maximum cost you set for the run was reached before any result came back. Raise it and run again. |
| `NO_RESULTS` | Nothing came back at all for the whole run. |
| `UNEXPECTED_ERROR` | The run stopped early on an unexpected error. The rows before this one are kept. |

### 💡 What people use it for

- Watching a set of creators: schedule the handles weekly and send the newest counts to Slack,
  Google Drive, Make, Zapier or n8n.
- Pulling the videos under a campaign hashtag, then reading the captions for how people talk about
  it.
- Keyword research: search a phrase and see which videos TikTok puts in front of people.
- A quick check on one video from a link somebody dropped in a chat.

From a hashtag to a creator shortlist, in three runs:

1. Run this on a hashtag in your niche and copy the `author.uniqueId` column.
2. Paste those handles into [TikTok Profile Scraper](https://apify.com/dami_studio/tiktok-user-scraper)
   and sort on `followerCount`.
3. Put the `webVideoUrl` of the videos worth a closer look into
   [TikTok Comments Scraper](https://apify.com/dami_studio/tiktok-comments-scraper) and read what the
   audience says.

### 🚧 What it does not do

- **A profile is not an archive.** About a dozen rows per handle, and no input changes that.
- **No comments and no follower lists.** Comments come from TikTok Comments Scraper.
- **The limit is shared across your inputs** unless you set `resultsPerInput`. One busy hashtag can
  use the whole run.
- **Subtitles only where the video has them.** Nothing is generated from the audio; no subtitles
  means `null`.
- **A private account still costs its profile row.** You get the profile, then a `NOT_FOUND` row
  saying the videos are private.
- **A profile's video can arrive with `null` counters.** It is still a delivered row and it is charged.
- **Media links expire.** They are signed and short lived.
- It is not an official TikTok tool. Dami's Studio is independent and is not affiliated with or endorsed by TikTok, or by any other company named on this page.

### 🧭 Which TikTok scraper do you need?

| If you want | Use |
|---|---|
| Profiles, hashtags, keyword search and single videos in one run | This one |
| Follower and like counts for a list of handles | [TikTok Profile Scraper](https://apify.com/dami_studio/tiktok-user-scraper) |
| Videos posted under a hashtag | [TikTok Hashtag Scraper](https://apify.com/dami_studio/tiktok-hashtag-scraper) |
| Videos using one sound | [TikTok Sound Scraper](https://apify.com/dami_studio/tiktok-music-scraper) |
| Comments under a video | [TikTok Comments Scraper](https://apify.com/dami_studio/tiktok-comments-scraper) |
| The hashtags ranking in a country right now | [TikTok Trending Hashtags](https://apify.com/dami_studio/tiktok-creative-center-trends) |

### ❓ Questions people ask

#### Do I need a TikTok account to scrape TikTok?

No. Everything is read signed out. The cookie field is optional.

#### Why did a profile only return 12 rows?

That is the ceiling for a profile, so no setting gets you the
full back catalogue. Hashtag and search modes are where the depth is.

#### Is resultsPerPage per input or per run?

Per run, the total across every input. For a limit on each one, set `resultsPerInput`.

#### Why do the numbers differ from what I see in the app?

The app rounds big counts and the row does not. Counts move by the minute on a video that is
doing well, so compare against the time of your run. If something is missing, open the page in a
private browser window: what a signed-out visitor sees there is what this actor can read.

#### Can I call it from code or connect it to an AI assistant?

The [API tab](https://apify.com/dami_studio/tiktok-scraper/api/python) has code for Python, JavaScript and the CLI.
MCP clients like Claude and ChatGPT use `https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/tiktok-scraper`;
coding assistants can load [our skill
file](https://api.apify.com/v2/key-value-stores/BTBHU3DdiuoF4JOsK/records/apify-tiktok-SKILL.md). All three bill your
Apify account at the same price.

```python
## pip install apify-client
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("dami_studio/tiktok-scraper").call(
    run_input={
        "hashtags": ["cats"],
        "resultsPerInput": 20,
    },
    max_total_charge_usd=0.10,  # the most this run may spend
)
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    if row.get("ok") is False:  # free note rows, not results
        continue
    print(row.get("playCount"), (row.get("text") or "")[:80])
```

#### Is scraping TikTok legal?

This reads public profiles and public videos only, never private accounts or messages. Results can
still contain personal data, which GDPR and similar laws cover, so have a reason for collecting it.
Apify's write-up on [the legality of web scraping](https://blog.apify.com/is-web-scraping-legal/) is
a good starting point, and we are not lawyers.

### 🆘 If something breaks

Open the **Issues** tab on the actor page. Include the handles, tags or links you used and the run
ID, and the log is usually enough for us to see what happened.

# Actor input Schema

## `profiles` (type: `array`):

TikTok usernames (with or without @) to scrape. Each one returns a profile row (followers, likes, bio, verified) plus that creator's ~10 newest videos.

## `hashtags` (type: `array`):

Hashtags (with or without #) to scrape videos from.

## `searchQueries` (type: `array`):

Keywords to search TikTok for.

## `startUrls` (type: `array`):

Paste TikTok URLs directly, a profile URL, a /video/ URL (single video), or a /tag/ URL (hashtag).

## `resultsPerPage` (type: `integer`):

Most rows the whole run returns, counted across all your profiles, hashtags, searches and links together, so the first input can use it up. You are charged per result returned. A single profile gives about 11 rows (1 profile + ~10 videos) however high this is set; hashtag and search page much deeper. Not used when Results per input is set.

## `resultsPerInput` (type: `integer`):

Optional. When set, each profile, hashtag, search and video link stops at this many rows, so every input gets its share, and Max results per run is not used. A run can then return up to this many rows for each input you list. Leave it empty to keep one limit for the whole run.

## `maxRunSeconds` (type: `integer`):

Safety cap on total scrape time. If a slow query (e.g. a search/feed that keeps paginating with few results) hits this, the run stops and returns what it has so far. Prevents runaway cost on pathological queries.

## `minLikes` (type: `integer`):

Optional add-on, off unless you set a value. Keeps only videos with at least this many likes. A video whose like count could not be read is left out. Leave it empty to keep every video.

## `maxLikes` (type: `integer`):

Optional add-on, off unless you set a value. Keeps only videos with at most this many likes. Leave it empty to keep every video.

## `postedAfter` (type: `string`):

Optional add-on, off unless you set a date. Keeps only videos posted on or after this day, counted in UTC, for example 2026-10-01. Leave it empty for no date limit.

## `postedBefore` (type: `string`):

Optional add-on, off unless you set a date. Keeps only videos posted before this day, counted in UTC: 2026-10-15 keeps videos up to the end of 14 October. Leave it empty for no date limit.

## `excludePinned` (type: `boolean`):

Profiles only. Leaves out the videos a creator pinned to the top of their profile, so you get just their newest ones. Each video it leaves out is billed as "Pinned video left out", see the Pricing tab. A pinned video that is also the creator's newest stays in.

## `includeSubtitles` (type: `boolean`):

Adds the subtitles TikTok itself publishes for each video, as plain text, with their language. Empty when TikTok has none for that video. Nothing is generated: no transcript is made from the audio. Hashtag and search videos need one more page read each, so those runs take longer.

## `notionConnector` (type: `string`):

Optional. Deliver the results to your connected Notion via Apify MCP connectors.

## `notionParentId` (type: `string`):

Optional Notion database/data-source id to create the pages in.

## `sessionCookies` (type: `array`):

Optional: your own session, used only for your run. You are responsible for using it within the site's terms. Runs work without it.

## `proxyConfiguration` (type: `object`):

Optional, and not needed for a normal run.

## Actor input object example

```json
{
  "profiles": [
    "nasa",
    "nba"
  ],
  "hashtags": [
    "spacex",
    "science"
  ],
  "searchQueries": [
    "rocket launch"
  ],
  "startUrls": [
    "https://www.tiktok.com/@nasa",
    "https://www.tiktok.com/@nasa/video/7300000000000000000"
  ],
  "resultsPerPage": 50,
  "maxRunSeconds": 90,
  "excludePinned": false,
  "includeSubtitles": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (type: `string`):

Scraped profiles and videos are stored in the default dataset (one row each). Empty or failed runs return a single uncharged diagnostic row instead.

# 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 = {
    "profiles": [
        "nasa"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/tiktok-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 = { "profiles": ["nasa"] }

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/tiktok-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 '{
  "profiles": [
    "nasa"
  ]
}' |
apify call dami_studio/tiktok-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/tiktok-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/QbQwalBHoBaZSPIN2/builds/FlNQyxS3aw0Zj8llm/openapi.json
