# TikTok Profile Scraper - Videos, Followers & Hashtag Views (`s-r/tiktok-profile`) Actor

Scrape public TikTok profiles by handle or link: follower, like and video counts, then every video with views, likes, comments, shares, caption, hashtags, sound and posting date. Also reads single videos by link, related videos and hashtag view totals. No login.

- **URL**: https://apify.com/s-r/tiktok-profile.md
- **Developed by:** [SR](https://apify.com/s-r) (community)
- **Categories:** Social media, Videos
- **Features:** Limited permissions
- **Stats:** 8 total users, 6 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 actor run starteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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 Profile Scraper - Videos, Followers & Hashtag Views

This TikTok profile scraper reads public TikTok accounts and every video on them:
followers, likes and video count per account, then one row per video with views,
likes, comments, shares, caption, hashtags, sound and posting date. Built for
creator research, campaign tracking and competitor monitoring.

Three things in one Actor, and you pick by what you fill in.

**Accounts.** Paste handles or profile links and get the account's followers,
likes and video count, then one row per video with views, likes, comments,
shares, caption, hashtags, sound and posting date.

**Single videos.** Paste video links or bare video ids to read just those.

**Hashtags.** Paste tags to get how many videos carry them and how many views
those have between them.

On top of that it can list the videos TikTok shows as **related** to a video you
give it, and pull from the public **Explore** feed.

No API key, no login, no browser.

### How to use the TikTok profile scraper

1. Paste one or more TikTok handles or profile links into **TikTok accounts**.
2. Set **Maximum videos per account** (default 50), or `0` for account details only.
3. Optionally add single video links, hashtags or related-video seeds.
4. Start the run. Video and hashtag rows land in the dataset; the per-account
   details and the run totals land in the `summary` record, and anything that
   could not be read lands in the `errors` record.

The same input works through the Apify API, so a scheduled run or a script call
gets exactly the same rows as a run from the Console.

### Input

| Field | Name in the API | Default | What it does |
|---|---|---|---|
| TikTok accounts | `profiles` | — (prefill `@nasa` and one profile link) | Handles or profile links, one per line |
| Maximum videos per account | `max_videos` | `50` | Videos per account, newest first; `0` reads only the account |
| Maximum accounts | `max_accounts` | `25` | Stop after this many accounts, counted after duplicates are removed |
| Single videos | `video_urls` | empty | Video links or bare video ids, read one by one |
| Hashtags | `hashtags` | empty | `#tag`, `tag` or a `/tag/` link; returns reach figures |
| Related videos | `related_to` | empty | Seed video links or ids; about 16 related videos per seed |
| Explore feed videos | `explore` | `0` | How many videos to pull from the public Explore feed; `0` disables it |
| Explore category | `explore_category` | `"0"` | Optional Explore category id; `0` is the default mixed feed |

A full input that uses every option looks like this:

```json
{
  "profiles": ["@nasa", "https://www.tiktok.com/@nasa?refer=embed"],
  "max_videos": 40,
  "max_accounts": 1,
  "video_urls": ["https://www.tiktok.com/@nasa/video/7682928988821490974"],
  "hashtags": ["https://www.tiktok.com/tag/olemiss"],
  "related_to": ["7682928988821490974"],
  "explore": 5,
  "explore_category": "0"
}
```

#### Paste the link however you copied it

A TikTok link copied from a video embedded on another site comes with a tail of
tracking:

```
https://www.tiktok.com/@that.sara.with.no.h?referer_url=www.ladbible.com%2F
&refer=embed&embed_source=121374463%2C121468991%2C121439635…
&referer_video_id=7682928988821490974
```

That works as it is. So does `@handle`, a bare handle, and a link to a single
video. Everything after the account name is dropped, so the same account pasted
four different ways is read once, not four times.

#### Filters and scale

- **TikTok accounts** — handles or links, one per line
- **Maximum videos per account** — the feed returns 35 per request, so this
  rounds up to whole requests and is the ceiling on both time and cost
- **Maximum accounts** — applied after duplicates are removed

Two accounts and 73 videos took **under thirteen seconds** in testing. Videos
come back newest first and paging is handled inside the run.

### Output: what the TikTok profile scraper returns

#### What you get per video

- `play_count`, `like_count`, `comment_count`, `share_count`, `collect_count`
- `description` (the caption) and `hashtags` pulled out of it separately
- `created_at` as a UTC timestamp, and `duration_seconds`
- `music_title`, `music_author` and `music_id` — which sound the post used
- `cover_url`, `width`, `height`, `is_ad`
- `handle`, `nickname`, `account_id` and `follower_count` on **every row**
- `video_id`, `url`, `position` and `record_type` (`video`)

That last point matters more than it looks. Running twenty accounts gives you
one table you can group and sort directly, instead of rows that have to be
joined back to an account list first.

Each row also carries the same numbers under cross-platform names (`post_id`,
`post_url`, `author_handle`, `author_id`, `text`, `posted_at`, `views`, `likes`,
`comments_count`, `shares`, `hashtags_normalized`, `platform`, `fetched_at`), so
TikTok rows line up with rows from our other social media scrapers without
renaming columns. Related videos also carry `seed_video_id`, the video they were
found from.

A real video row, from a run on 2 October 2026 (cover link shortened):

```json
{
  "video_id": "7665075736742530317",
  "url": "https://www.tiktok.com/@nasa/video/7665075736742530317",
  "description": "Something big just landed on TikTok.",
  "created_at": "2026-07-21T20:09:02Z",
  "play_count": 1500000,
  "like_count": 97500,
  "comment_count": 2886,
  "share_count": 3171,
  "collect_count": 7192,
  "duration_seconds": 25,
  "width": 720,
  "height": 1280,
  "cover_url": "https://p16-common-sign.tiktokcdn-us.com/tos-useast5-p-0068-tx/oAUhkWfCZC7g6ALj2HZgOeELFvIrALeFYOoGI4~tplv-tiktokx-origin.image?…",
  "hashtags": null,
  "music_title": "original sound",
  "music_author": "NASA",
  "music_id": "7665075815390645006",
  "is_ad": false,
  "account_id": "7664638705177150477",
  "handle": "nasa",
  "nickname": "NASA",
  "follower_count": 1900000,
  "position": 1,
  "record_type": "video",
  "post_id": "7665075736742530317",
  "author_handle": "nasa",
  "views": 1500000,
  "likes": 97500,
  "comments_count": 2886,
  "shares": 3171,
  "posted_at": "2026-07-21T20:09:02Z",
  "platform": "tiktok",
  "fetched_at": "2026-10-02T15:29:43.482Z"
}
```

#### What you get per account

Followers, following, total likes and video count, plus the bio, verified flag,
whether the account is private, and the avatar. These are reported in the run
summary for every account read, and the identity fields ride along on each video
row.

From the same day, the `summary` entry for `@nasa`: `follower_count` 1,900,000,
`following_count` 23, `like_count` 10,100,000, `video_count` 51, `verified`
true, `private_account` false, bio "Making the seemingly impossible, possible.✨".

Set **Maximum videos per account** to `0` to read only the accounts and skip
videos entirely. That is the cheap way to track follower counts over time.

#### Single videos, by link or by id

Paste a video link and only that video is read. The handle in the link is
ignored, because `/@x/video/<id>` serves the same page as the real one: the id
is what resolves. So a link with a stale handle still works, and the same video
pasted twice is read once.

A single-video row carries everything an account's video row does, plus the
author's follower count **at the moment of the run**, which is what makes a
one-off link worth reading on its own.

#### Hashtags: reach figures, and only reach figures

Paste `#olemiss`, `olemiss` or a `/tag/` link and you get the hashtag's id, how
many videos carry it and how many views they have between them. Measured live:
**#olemiss, 240,700 videos, 3.44 billion views**; **#hottytoddy, 45,241 videos,
499 million views**. A month later, on 2 October 2026, the same tags stood at
258,988 videos and 3.79 billion views, and 49,941 videos and 531 million views.

A real hashtag row from that run:

```json
{
  "hashtag_id": "72512",
  "hashtag": "olemiss",
  "url": "https://www.tiktok.com/tag/olemiss",
  "description": null,
  "video_count": 258988,
  "view_count": 3787755695,
  "is_commerce": false,
  "record_type": "hashtag",
  "topic": "olemiss",
  "post_count": 258988,
  "position": 2
}
```

What you do **not** get is the tag's video list. TikTok does not serve it: the
endpoint that would list them answers with zero items, to every client tried.
Rather than return an empty video list that reads as an empty hashtag, the run
returns the counts and reports `hashtagVideosAvailable: false`.

If you want videos for a topic, the way that works is to read the accounts that
post about it and filter their captions on the tag, which the `hashtags` field
on every video row supports directly.

#### Related videos and the Explore feed

**Related videos** (`related_to`) takes video links or ids and returns the
videos TikTok lists next to each one, about 16 per seed, each a full video row
with its own author and stats plus `seed_video_id`.

**Explore** (`explore`) pulls videos from TikTok's public discover feed. It is
best effort: TikTok often serves this feed empty to anyone who is not signed in.
When it does, the run says so in `errors` and you pay nothing for it.

#### Two counts that will not always match, on purpose

An account's profile states how many videos it has. The feed hands over what it
is willing to serve, and those are not always the same number: restricted,
region-limited and recently removed posts are counted but not served.

Rather than smooth that over, the run summary reports all of it:

- `videosClaimedByProfiles` — what the profiles say they hold
- `videosReturned` — what actually came back
- `maxVideosPerAccount` and `cappedByMaxVideos` — so you can tell your own limit
  apart from a restriction
- `accountsThatServedNoVideos` — accounts that claim videos and served none

Asking for 40 videos from an account holding 47 is not a restriction, and
without the cap in the summary those two cases look identical.

The summary also holds `accountsRequested`, `accountsRead`, `totalPlays`,
`totalLikes`, `accounts` (the per-account details above), `fetchedInSeconds` and
`errorCount`. A run without accounts, only single videos, hashtags, related
videos or Explore, reports `videosRequested`, `videosReturned`,
`hashtagsRequested`, `hashtagsReturned`, `hashtagVideosAvailable`,
`relatedRequested`, `relatedReturned`, `exploreRequested` and `exploreReturned`
instead.

#### Empty is never assumed

The video feed is the kind of endpoint that answers awkward requests with a
success and nothing in it. This Actor treats a body with nothing in it as a
failure to be retried and reported, never as an account with no videos. A
genuinely empty account is reported as exactly that, separately.

The same applies to individual numbers: a missing view count comes back empty
rather than as `0`, because zero views is a real and different thing from a
count that was not published.

### What people use this for

**Creator research.** Follower count next to per-video engagement is what
separates an account with reach from an account with an audience. The gap
between plays and likes across an account's recent posts says more than any
follower number.

**Campaign tracking.** Run the accounts in a campaign on a schedule and keep the
rows. Because `created_at` and every count are on each row, growth per post over
time falls out of the table without extra work.

**Follower count tracking.** A run with **Maximum videos per account** set to
`0` is a bulk TikTok follower count checker: twenty-five accounts, one run, the
follower, like and video count of each, for the price of the run start.

**Sound and hashtag trends.** `music_id` and `hashtags` across many accounts show
what is spreading and how fast, which is the input to deciding what to post.

**Competitive monitoring.** The same shape of data for your accounts and
somebody else's, in one table, with the account on every row.

### How this TikTok scraper compares

| | This Actor | TikTok's own analytics | Follower count websites | Other Store scrapers |
|---|---|---|---|---|
| Any public account, not just your own | yes | no | yes | yes |
| Per-video views, likes, comments, shares, saves | yes | your own only | no | yes |
| Account on every video row | yes | n/a | n/a | varies |
| Hashtag view totals | yes | no | no | separate Actor |
| Bulk and scheduled through an API | yes | no | no | yes |
| Price | $0.002 per run + $0.0015 per row | free | free | see below |

Measured on the Apify Store on 2 October 2026: `clockworks/tiktok-profile-scraper`
charges $0.003 per result on the free plan (lower on paid plans),
`clockworks/tiktok-scraper` $0.0037. This Actor charges one rate for every user
and keeps the account details, the hashtag lookups and the single-video reads in
the same run.

### What this does not cover

Videos are read from the account's own feed, so this returns **what an account
posted**, not what it was tagged in, and not its liked or saved posts. Comments
are counted but not fetched: a post with 146 comments gives you the number, not
the 146 texts.

**There is no keyword search**, and that is not a gap in this Actor. TikTok's
search endpoint answers with `Please login your account first`, so nobody reads
it without an account. Accounts, videos and hashtags are what is available
without one.

Sounds are identified rather than downloaded. `music_id` is stable across posts,
which is what makes it useful for tracking a sound's spread, but the audio
itself is not part of the output. Videos are not downloaded either: you get the
link and the cover image.

### Pricing

Pay per event, nothing else:

- **Actor run started** — $0.002, once per run
- **Video returned** — $0.0015 per row in the dataset (a video row, or a hashtag row)

A run that reads 50 videos from one account costs $0.002 + 50 × $0.0015 =
**$0.077**. A run with `max_videos` at `0` writes no rows and costs only the
$0.002 start fee, however many accounts it reads. Accounts that do not exist,
videos that are gone and an empty Explore feed produce no rows and are not
charged.

On the free Apify plan a run returns at most 10 rows; paid plans have no cap.

### FAQ

**Do I need a TikTok account, cookies or an API key?**
No. Only public data is read, and nothing you enter is used to sign in anywhere.

**Can I scrape private TikTok accounts?**
No. Only public accounts can be read. A private account is reported with its
profile details and no videos, which is the honest answer rather than an empty
result.

**How do I check the follower count of many TikTok accounts at once?**
Paste the handles, set **Maximum videos per account** to `0` and run. The
`summary` record lists every account with its follower, following, like and
video count.

**Why did I get fewer videos than the profile says it has?**
The profile counts restricted, region-limited and removed posts that the feed
does not serve. Compare `videosClaimedByProfiles`, `videosReturned` and
`cappedByMaxVideos` in the summary to see which it was.

**Can I get the videos for a hashtag?**
Not directly: TikTok does not serve a hashtag's video list without a login. You
get the hashtag's video count and view total, and you can filter account videos
on the `hashtags` field.

**What happens with a handle that does not exist?**
The run still succeeds. The handle is listed by name in the `errors` record, it
produces no rows and it is not charged.

**Can I call this TikTok profile scraper from my own code?**
Yes. Start it through the Apify API with the same JSON input as above and read
the dataset when it finishes, or schedule it in the Console.

### Notes

Counts are live and move constantly on popular posts; a run is a snapshot, which
is why the posting date is on every row.

An account that does not exist is reported by name, so a run of fifty tells you
exactly which handle was wrong rather than silently returning fewer rows.

# Actor input Schema

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

Handles or profile links, one per line. A link copied from an embedded video works as it is: everything after the handle is ignored, so ?referer_url=...\&refer=embed resolves to the same account as a plain @handle. Leave empty when you only want single videos or hashtags.

## `max_videos` (type: `integer`):

How many videos to read per account, newest first. Set to 0 to read only the account details and no videos. The feed returns 35 per request, so this rounds up to whole requests and is the cost ceiling.

## `max_accounts` (type: `integer`):

Stop after this many accounts, counted after duplicates are removed.

## `video_urls` (type: `array`):

Video links or bare video ids to read one by one. The handle in a video link is ignored because the id is what resolves, so a link with a stale handle still works. Returns the same fields as an account's videos, plus the author's follower count at the moment of the run.

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

Hashtags to look up, as #tag, tag or a /tag/ link. Returns how many videos carry the tag and how many views they have between them. TikTok does not serve the tag's video list to anyone, so this is reach figures only and the run says so rather than returning an empty list.

## `related_to` (type: `array`):

Seed video links or bare ids. For each, returns the videos TikTok lists as related, about 16 per seed, each a full record with its own author and stats and a seed_video_id column. A cheap way to widen coverage from a starting video without a login.

## `explore` (type: `integer`):

How many videos to pull from TikTok's public Explore/discover feed. 0 disables it. Note: unlike the other modes this endpoint may require the signed browser path; when it refuses, the run reports the reason rather than an empty feed.

## `explore_category` (type: `string`):

Optional Explore category id (categoryType). Leave as 0 for the default mixed feed.

## Actor input object example

```json
{
  "profiles": [
    "@nasa"
  ],
  "max_videos": 50,
  "max_accounts": 25,
  "video_urls": [
    "https://www.tiktok.com/@nasa/video/7682928988821490974"
  ],
  "hashtags": [
    "#olemiss"
  ],
  "related_to": [
    "https://www.tiktok.com/@nasa/video/7682928988821490974"
  ],
  "explore": 0,
  "explore_category": "0"
}
```

# Actor output Schema

## `videos` (type: `string`):

One row per video.

## `summary` (type: `string`):

Accounts read with their follower and video counts, videos returned, and total plays and likes.

## `errors` (type: `string`):

Accounts that could not be read.

# 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",
        "https://www.tiktok.com/@that.sara.with.no.h"
    ],
    "video_urls": [],
    "hashtags": [],
    "related_to": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/tiktok-profile").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",
        "https://www.tiktok.com/@that.sara.with.no.h",
    ],
    "video_urls": [],
    "hashtags": [],
    "related_to": [],
}

# Run the Actor and wait for it to finish
run = client.actor("s-r/tiktok-profile").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",
    "https://www.tiktok.com/@that.sara.with.no.h"
  ],
  "video_urls": [],
  "hashtags": [],
  "related_to": []
}' |
apify call s-r/tiktok-profile --silent --output-dataset

```

## MCP server setup

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

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/f22pT3WwXJhY4j5Md/builds/T0f8wZPMQSWh0hAWL/openapi.json
