# TikTok Profile Scraper – Posts, Views & Followers (`simple.actor/tiktok-profile-posts`) Actor

Scrape any public TikTok profile for its latest posts — caption, hashtags, views, likes, comments, shares, video files, photo carousels and the sound used — plus the account record with exact follower count. No login, no cookies, no API key. Up to 13 latest posts per profile. Unofficial.

- **URL**: https://apify.com/simple.actor/tiktok-profile-posts.md
- **Developed by:** [Simple Actor](https://apify.com/simple.actor) (community)
- **Categories:** Social media, Videos, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.50 / 1,000 profile reads

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## TikTok Profile Scraper – Posts, Views & Followers

Scrape any public TikTok profile for what it has posted lately — caption,
hashtags, mentions, views, likes, comments, shares, the video file, photo
carousels, the sound used and the full account record with its exact follower
count. **Up to 13 latest posts per profile**, newest first, 45 fields each.
Built for social media monitoring, influencer research, competitor tracking and
hashtag analysis across many accounts at once.

**$0.50 per 1,000 profiles read** — Apify platform usage and proxy included,
nothing else to pay. The charge is per profile and does not depend on how many
posts come back, so a full read and a check that finds one new post cost the
same. A profile that cannot be read is never charged.

**No login, no cookies, no API key, no account of yours involved.** There is no
session to keep alive and nothing of yours to get restricted.

*Unofficial. Not affiliated with, endorsed by, or sponsored by TikTok or
ByteDance.*

> **Read this before you start: up to 13 posts per profile**, usually 10 to 12.
> That is what TikTok shows a logged-out reader, not a setting — there is no
> cursor to ask for more. This is built to check *many* accounts *often*. If you
> need one creator's back catalogue, this is the wrong tool.

### Features

- **Latest posts per public profile** — up to 13, newest first, 45 fields each.
- **Full engagement metrics** — play count, likes, comments and shares on every
  post.
- **Exact follower counts** — read from the profile page, not TikTok's rounded
  embed figure, with `followersAreExact` on every row saying which you got.
- **Video and photo posts both handled** — a video file URL and cover, or an
  `images` array for a photo carousel.
- **Hashtags and mentions from TikTok's own entity list**, not guessed from the
  caption — so a mention returns the handle actually tagged, not the display
  name TikTok writes into the text.
- **The sound used** — `musicName` on every post.
- **On-screen sticker text** — `stickerTexts` captures overlaid captions.
- **Pinned posts flagged** — `isPinned`, and rows sorted by real date, so an old
  pinned post never passes for the newest.
- **Handles or URLs** — bare `nasa`, `@nasa`, a profile link, or even a link to
  one of the account's videos.
- **Incremental polling** — `onlyPostsNewerThan` takes `3 days`, `12 hours` or
  an ISO date.
- **A fast, light listing mode** — turn post details off to skip one request per
  post.
- **Runs stay green.** A missing, private or mistyped account is a row, not a
  failed run.

### Input

```json
{
  "startUrls": [{ "url": "https://www.tiktok.com/@nasa" }],  // profile or video URLs
  "handles": ["nasa", "@natgeo"],   // or bare handles — both lists are read together
  "maxPosts": 13,                   // latest posts per profile (max 13)
  "onlyPostsNewerThan": "3 days",   // optional window: "12 hours", "3 days", "2026-08-01"
  "includePostDetails": true,       // one extra request per post for engagement counts
  "includeProfileDetails": true,    // read the profile page for exact follower counts
  "proxy": { "useApifyProxy": true }
}
```

Either input works alone, or both together:

| Input | Type | Default | What it does |
|---|---|---|---|
| `startUrls` | array | — | Public TikTok profile URLs. A link to one of the account's videos works too — it is read as the account. |
| `handles` | array | — | Handles without the URL: `nasa` or `@nasa`. Read together with `startUrls`. |
| `maxPosts` | integer | `13` | Latest posts per account. Capped at 13 — TikTok renders one batch to a logged-out reader. Lowering it does not make the run cheaper. |
| `onlyPostsNewerThan` | string | — | Keep only posts after a window or ISO date. |
| `includePostDetails` | boolean | `true` | One extra request per post for likes, comments, shares, TikTok's own posted time, the sound, hashtags and carousel images. |
| `includeProfileDetails` | boolean | `true` | Read the profile page for exact follower and like counts instead of the embed's rounded figures. |
| `proxy` | object | Apify datacenter | Datacenter is enough — TikTok's embed served every datacenter request tested without a challenge. |

### Output

One row per post, newest first. Every row carries the full account record, so a
row stands on its own.

```jsonc
{
  "id": "7675478347278863629",
  "url": "https://www.tiktok.com/@nasa/video/7675478347278863629",
  "type": "video",
  "caption": "Thanks a million! 🌖🚀",
  "hashtags": [],
  "mentions": [],
  "timestamp": "2026-08-18T20:56:38.000Z",
  "isPinned": false,
  "playCount": 1500000,
  "likeCount": 177100,
  "commentCount": 8831,
  "shareCount": 4208,
  "ownerUsername": "nasa",
  "videoUrl": "https://…",
  "coverUrl": "https://…",
  "musicName": "original sound - NASA",
  "stickerTexts": ["us realizing there's already 1M of you following us"],
  "author": {
    "username": "nasa",
    "nickname": "NASA",
    "followerCount": 1162398,
    "followersAreExact": true,
    "heartCount": 5432780,
    "postCount": 32,
    "isVerified": true
  }
}
```

Photo posts come back as `"type": "photo"` with an `images` array instead of a
video file. Hashtags and mentions are read from TikTok's own entity list, not
guessed from the caption — TikTok writes a mention into the caption using the
account's *display name*, so a post tagging `@wash_wizards` reads
"@Washington Wizards" in the text. The list gives you the handle that was
actually tagged.

### How to use

#### From Apify Console

1. Open the Actor and click **Try for free** / **Start**.
2. Paste profile links into **Profile URLs**, or a column of bare handles into
   **Handles** — the two lists are read together.
3. Leave **Read each post's full detail** and **Read exact follower counts** on
   unless you specifically want a fast listing; neither changes the price.
4. To poll for what is new, set **Only posts newer than** to a window shorter
   than the gap between your runs.
5. Click **Start**, then open the **Dataset** tab and export as JSON, CSV or
   Excel.

**Batch your accounts** — fifty handles in one run is much cheaper per profile
than fifty runs of one.

#### From the API

```bash
curl -s "https://api.apify.com/v2/acts/simple.actor~tiktok-profile-posts/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"handles": ["nasa"]}'
```

Monitoring a list of creators, with the JavaScript client:

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('simple.actor/tiktok-profile-posts').call({
    handles: ['nasa', 'natgeo', 'nba'],
    onlyPostsNewerThan: '24 hours',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();

for (const row of items) {
    if (row.error) { console.warn(row.error, row.errorDescription); continue; }
    if (row.isPinned) continue;            // pinned posts can be months old
    console.log(row.ownerUsername, row.playCount, row.caption);
}
```

### Use cases

- **Monitor creator and competitor accounts** — poll a list of handles on a
  schedule with `onlyPostsNewerThan` and see new posts and their engagement as
  they land.
- **Influencer research and vetting** — exact follower counts, total likes, post
  counts and recent engagement across a shortlist in one run.
- **Engagement benchmarking** — compare views, likes, comments and shares per
  post across accounts in the same niche.
- **Hashtag and sound tracking** — hashtags and mentions come from TikTok's own
  entity list, and `musicName` shows which sounds an account is riding.
- **Campaign and brand-mention monitoring** — check which accounts a creator
  actually tagged, by handle rather than display name.
- **Media archiving** — collect video files, covers and photo-carousel images
  (download promptly — the URLs expire within hours).

### Usage notes

**A pinned post is not the newest post.** TikTok shows pinned posts first
whatever their age — one account tested pins a post a month older than the rest.
Rows are sorted by date and the pinned ones are flagged `isPinned`, so you can
sort or filter without being caught by it.

**`videoUrl` expires.** TikTok signs its media links with a few hours' validity.
Download the file promptly if you need it; store `url` as the permanent address.

**Follower counts: exact or rounded.** TikTok's embed rounds — it reports an
account with 1,162,398 followers as "1,200,000", which will not move as the
account grows. This Actor reads the profile page for the real figure, and every
row says which it got in `author.followersAreExact`. If that is `false`, the
counts are rounded to two significant figures.

**Views are always rounded.** TikTok itself rounds `playCount`; there is no
source for an exact figure.

**Posted times are exact when post details are read.** With details off, the
time is recovered from the post's own ID — accurate to about a minute, and
flagged `timestampIsApproximate`.

**Private accounts return an error, not an empty list.** So do accounts that no
longer exist. If you run several accounts at once, one that cannot be read
becomes its own row carrying `error` and `errorDescription`, and the rest still
deliver. An empty result always means "read fine, nothing to report".

**The run succeeds even when an account cannot be read.** An account that does
not exist, one that is private, a string TikTok could never have issued as a
handle, or a window reaching further back than one batch shows — each is an
answer about that account, and each comes back as a row on a run that ends
green. A run fails only if **not one** account could be reached at all, which
means TikTok or the proxy rather than your accounts.

So the error rows are the failure channel, not the run status:

> **If your pipeline acts on emptiness — marking an account quiet, retiring a
> handle — key off the presence of an `error` row, never off whether the run
> succeeded.**

| `error` | what happened |
|---|---|
| `not_found` | no such account, or TikTok refused the handle |
| `private` | the account exists but its posts need a login |
| `bad_input` | the text given could never be a TikTok handle, or a date could not be read |
| `window_too_wide` | the window reaches further back than one batch shows — use a shorter window, or run more often |
| `read_failed` | TikTok could not be reached; this is the only kind that can fail a run |

#### What it costs

**$0.50 per 1,000 profiles read.** One charge per profile, whatever the run
finds. Posts are free: reading a profile costs the same whether thirteen posts
come back or one, so charging for them would bill you for something that is not
what costs.

Two things follow from that:

- **Batch your accounts.** Fifty handles in one run is much cheaper per profile
  than fifty runs of one, because each run pays for its own startup.
- **Take all the posts.** Asking for fewer saves you nothing; the price is for
  reading the profile.

Because the charge is per profile, what you pay per *post* depends on how many
come back:

| Posts returned | Works out at |
|---|---|
| 13 — a full batch | $0.04 per 1,000 posts |
| 11 — a typical account | $0.05 per 1,000 posts |
| 1 — a poll that found one new post | $0.50 per 1,000 posts |

If you are polling with **Only posts newer than** and most checks come back
empty, budget by **profiles checked**, not posts collected — that is what you
are billed on. A profile that cannot be read is never charged, so a list
carrying dead or private accounts costs only for the live ones.

Turning off **Read each post's full detail** makes a run faster and lighter but
does not change the price, so leave it on unless you genuinely only want a
listing.

### FAQ

**Is scraping TikTok legal?**
This Actor reads posts that are publicly visible to anyone without logging in,
through TikTok's own embed surface. It does not log in, use cookies, or reach
private accounts. TikTok's Terms of Service restrict automated collection, so
check the platform's ToS and your own obligations, particularly around personal
data, before using it.

**Do I need a TikTok login, cookies or an API key?**
No. There is no session to keep alive and nothing of yours to get restricted.

**Can I scrape a creator's whole back catalogue?**
No. TikTok renders one batch to a logged-out reader — usually 10 to 12 posts,
13 at most — and offers no cursor to ask for more. This Actor is built to check
*many* accounts *often*; for one creator's full history it is the wrong tool.

**Does it support pagination?**
There is no next page to request, so no. Run the Actor on a schedule with
`onlyPostsNewerThan` to follow an account over time instead.

**Why is the follower count rounded?**
TikTok's embed rounds to two significant figures — it reports 1,162,398 as
"1,200,000", which will not move as the account grows. `includeProfileDetails`
is on by default and reads the profile page for the real figure. Every row says
which it got in `author.followersAreExact`.

**Are view counts exact?**
No. TikTok itself rounds `playCount`, and there is no source for an exact
figure.

**Why is the newest row not the account's newest post?**
It is — but pinned posts are the trap. TikTok shows them first whatever their
age, so rows here are sorted by real date and pinned ones flagged `isPinned`.
Filter on that field rather than taking the first row.

**Can it read private accounts?**
No. A private account comes back as a row with `error: "private"`, and the run
still succeeds.

**Why did my run succeed when an account was not read?**
By design. A missing account, a private one, a string TikTok could never have
issued as a handle, or a window reaching further back than one batch shows are
all answers about that account, not faults — each is a row on a green run. Only
`read_failed` on *every* account fails a run, which means TikTok or the proxy
rather than your accounts.

**Do I save money by asking for fewer posts?**
No. The charge is per profile, so reading a profile costs the same whether
thirteen posts come back or one. Take all the posts.

# Actor input Schema

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

Public TikTok profile URLs, one per account, e.g. https://www.tiktok.com/@nasa. A link to one of the account's videos works too — it is read as the account. This field only accepts real URLs; to give bare handles like "nasa", use the Handles field below. You can use both together.

## `handles` (type: `array`):

TikTok handles without the URL, e.g. nasa or @nasa. Use this instead of Profile URLs when you have handles rather than links; the two lists are read together.

## `maxPosts` (type: `integer`):

How many of the latest posts to return per account. TikTok renders one batch to a logged-out reader and offers no way to ask for more, so this is capped at 13 — most accounts return 10 to 12. Lower it only if you want fewer rows; it does not make the run cheaper.

## `onlyPostsNewerThan` (type: `string`):

Keep only posts published after this moment — ideal for incremental polling. Accepts an ISO date (2026-08-01) or a window ("3 days", "12 hours"). If the account's latest batch does not reach back that far, that account comes back as a row with error "window\_too\_wide" rather than a partial list that would read as complete; the run itself still succeeds. Shorten the window, or poll more often.

## `includePostDetails` (type: `boolean`):

On by default. Reads one extra request per post to get its like, comment and share counts, TikTok's own posted time, the music, the hashtags and mentions, and the images of a photo post. Turn it off for a fast, cheap listing: you keep the caption, view count, cover and video file, but engagement counts become null and the posted time is derived from the post id (accurate to about a minute).

## `includeProfileDetails` (type: `boolean`):

On by default. Reads the profile page for the account's exact follower and like counts plus its fuller record. With this off — or if TikTok challenges the page — the counts come from the embed and are rounded to two significant figures (1,200,000 rather than 1,161,914), which will not move as the account grows. Every item says which it got in "author.followersAreExact".

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

Apify Proxy settings. The default datacenter group is enough — TikTok's embed served every datacenter request tested without a challenge, and showed no rate limiting. Switch to residential only if accounts start coming back as read failures.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.tiktok.com/@nasa"
    }
  ],
  "handles": [
    "nasa"
  ],
  "maxPosts": 13,
  "onlyPostsNewerThan": "3 days",
  "includePostDetails": true,
  "includeProfileDetails": true,
  "proxy": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `posts` (type: `string`):

One row per post with its headline numbers.

## `engagement` (type: `string`):

Views, likes, comments, shares and hashtags per post.

## `media` (type: `string`):

Video files, covers, photo carousels and the sound used.

## `profile` (type: `string`):

The account record behind these posts.

## `raw` (type: `string`):

Every field of every item, exactly as pushed.

# 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 = {
    "startUrls": [
        {
            "url": "https://www.tiktok.com/@nasa"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("simple.actor/tiktok-profile-posts").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 = { "startUrls": [{ "url": "https://www.tiktok.com/@nasa" }] }

# Run the Actor and wait for it to finish
run = client.actor("simple.actor/tiktok-profile-posts").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 '{
  "startUrls": [
    {
      "url": "https://www.tiktok.com/@nasa"
    }
  ]
}' |
apify call simple.actor/tiktok-profile-posts --silent --output-dataset

```

## MCP server setup

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

```

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/fhlR8OB7KzyQpfzJ3/builds/0jnI68kwW1lbnlvxn/openapi.json
