# TikTok Profile Scraper - Videos & New Post Monitor (`neverempty/tiktok-profile-videos-scraper`) Actor

Export every video a TikTok account posted: caption, post time, views, likes, comments, shares, saves, duration, sound, hashtags, pinned and photo posts, with a date range. Monitoring mode returns only videos posted since the last run. Unofficial. Public data only.

- **URL**: https://apify.com/neverempty/tiktok-profile-videos-scraper.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Social media, Videos, Automation
- **Stats:** 13 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.92 / 1,000 video returneds

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 Profile Scraper - Videos & New Post Monitor

Export the videos a TikTok account has posted, newest first: **the video URL and id, the caption, the time it was posted, the view, like, comment, share and save counts, the length, the sound, the hashtags and mentions**, plus whether it is a photo post, whether it is pinned, TikTok's isAd flag and the tagged place. Set a date range to export one period only.

Turn on **monitoring mode**, schedule the Actor, and each run returns **only the videos posted since the last run**, so you pay for new posts instead of the whole profile every time. Accounts that do not exist and private accounts come back as free rows that say so.

Unofficial. Public data only. This Actor is not affiliated with TikTok. It reads the videos TikTok shows to any visitor of a public profile without logging in, and it does not read private accounts, direct messages or anything behind a login.

### What you can use it for

- **Competitor and creator tracking**: schedule monitoring mode on a list of accounts and get each new post with its first view and like counts every hour or day.
- **Influencer vetting**: export a creator's last 100 or 1,000 videos with views, likes, comments, shares and saves to see how their posts actually perform.
- **Content research**: collect captions, hashtags, sounds and post times of a niche's accounts for a spreadsheet, a database or an AI pipeline.
- **Campaign reporting**: export the videos an account posted inside a campaign's dates with `postedAfter` and `postedBefore`.

### How it works

1. Give usernames (`nasa`, `@nasa`), profile URLs (`https://www.tiktok.com/@nasa`) or secUids (`MS4wLjABAAAA...`).
2. For a username, the Actor opens the public profile page once to find the account's secUid, its video count and whether it is private.
3. It then reads the account's public video list (`https://www.tiktok.com/api/creator/item_list/`, 15 videos per page, newest first) page by page and returns each video once. Only videos that TikTok labels with the account you asked for are returned.
4. Accounts that do not exist, private accounts, verification pages and invalid input get a free row that says why.

### Input

| Field | Default | What it does |
| --- | --- | --- |
| `profiles` | example account | Usernames, profile URLs or secUids, one per line (commas and spaces also separate entries). A video URL of the account works too. A repeated account is read and charged once; the repeat gets a free `duplicate` row. Short links (`vm.tiktok.com`) are not followed. Up to 200 accounts per run. |
| `maxVideosPerProfile` | `100` | With monitoring off, stop an account after this many videos, newest first. In monitoring mode it limits only the first check of an account. Up to 10,000. |
| `postedAfter` | none | Only videos posted on or after this date (`2026-09-01`, read as 00:00 UTC), time (`2026-09-01T12:00:00Z`) or number of days back (`30 days`). Older videos are not returned or charged; reading an account stops once it reaches videos 3 days older than this. |
| `postedBefore` | none | Only videos posted before this date or time. Reading starts there, so an older period can be exported without paying for newer videos. Not available in monitoring mode. |
| `monitoringMode` | `false` | Return only videos posted since the last run of each account. |
| `resetMonitoringState` | `false` | Forget everything remembered and start again with a first check. Turn it off again after one run. |
| `useProxy` | `true` | Retry a page through a US proxy only if TikTok answers it with a rate limit or an empty response. |

```json
{
    "profiles": [
        "nasa",
        "https://www.tiktok.com/@duolingo"
    ],
    "maxVideosPerProfile": 50,
    "monitoringMode": true
}
```

If you leave `profiles` out of the input with monitoring off, the example account in the input form is read (its URL appears in `inputProfile` and the run log says so). In monitoring mode `profiles` is required, so a schedule never pays to watch the example. An empty list is rejected with a free `invalid-input` row.

### Output

One row per video or photo post. This row was read from Apify on 2026-09-29 (UTC):

```json
{
    "status": "ok",
    "inputProfile": "duolingo",
    "username": "duolingo",
    "usernameMatchesInput": true,
    "authorId": "6917704832925746181",
    "authorSecUid": "MS4wLjABAAAAxzJGbseZk0TDBgT88nlMt8s4rjFp-Um-B7X545vT-MaVZiZEWVyKmaVwqxntcGgn",
    "authorNickname": "Duolingo",
    "authorVerified": true,
    "profileVideoCount": 1187,
    "profileFollowerCount": 18158622,
    "videoId": "7690680991387553038",
    "videoUrl": "https://www.tiktok.com/@duolingo/video/7690680991387553038",
    "postType": "video",
    "description": "i wish i were a bird 🎨: @brerbo",
    "language": "en",
    "createdAt": "2026-09-28T20:10:30.000Z",
    "createTime": 1790626230,
    "playCount": 361200,
    "likeCount": 76600,
    "commentCount": 1191,
    "shareCount": 11000,
    "saveCount": 10437,
    "durationSeconds": 21,
    "videoWidth": 720,
    "videoHeight": 1280,
    "imageCount": null,
    "photoTitle": null,
    "coverUrl": "https://p19-common-sign.tiktokcdn-us.com/...",
    "musicId": "6642970913400687366",
    "musicTitle": "sonido original",
    "musicAuthor": "Armando Vialpando",
    "musicIsOriginal": true,
    "hashtags": [],
    "mentions": [
        "brerbo_"
    ],
    "isPinned": false,
    "isAd": false,
    "locationName": null,
    "locationAddress": null,
    "locationCity": null,
    "isFirstCheck": null,
    "isNew": null
}
```

Every row also has `source` and `scrapedAt`. Notes on the columns:

- **The counts are the ones TikTok shows, and TikTok rounds large ones**: a view count of 8,700,000 is TikTok's rounded figure, not an exact count. `saveCount` comes from TikTok's second set of counts, which is less rounded (10,845 where the first set says 10,800).
- `postType` is `photo` for photo posts (slideshows): they have `imageCount` and `photoTitle`, and `durationSeconds`, `videoWidth` and `videoHeight` are `null` (TikTok reports a length of 0 for them). Some old videos have no length either: on 2026-09-29, 164 of @scout2015's videos from 2016 to 2018 came without a length (99 of them also without width and height), so those are `null` too.
- `profileVideoCount` and `profileFollowerCount` are the account's counts from its profile page, repeated on each row. They are `null` when you gave a secUid, because the profile page is then not opened. `profileVideoCount` is TikTok's unrounded figure but can differ from the number of videos listed (see below).
- `isAd` is TikTok's own `isAd` flag on the post, returned as it is. TikTok does not document it; on the posts checked it matched TikTok's ad-authorization flag, so it does not prove that the post ran as a paid ad.
- `isPinned` is `true` for posts the creator pinned to the profile.
- `hashtags` and `mentions` come from the tags TikTok attaches to the caption. `mentions` holds the usernames, which can differ from the name written in the caption.
- `coverUrl` is a signed TikTok image link that expires after a few days; download the image if you need to keep it.
- `usernameMatchesInput` is `false` when TikTok answers with a different username than the one you gave (for example after a rename).
- `isFirstCheck` and `isNew` are filled only in monitoring mode (see below).

#### How many videos you get

The list reaches back to an account's first post. On 2026-09-29, reading whole accounts from Apify returned **1,187 videos for @duolingo** (the profile showed 1,187), **3,113 for @scout2015** (the profile showed 3,071) and **49 for @nasa** (the profile showed 49), with no repeats; the 4,300 rows of @duolingo and @scout2015 took 282 seconds. `profileVideoCount` is on every row so you can compare. Photo posts are included (210 of @duolingo's 1,187).

TikTok lists the videos newest first, but not strictly: in the same read, 5 of @scout2015's 3,113 videos were listed after older ones, by up to 12.5 days (this looks like posts scheduled in advance), and none of @duolingo's. So `postedAfter` and monitoring mode keep reading until they reach videos 3 days older than their boundary; a video listed more than 3 days out of place can still be missed there.

### Rows that are never charged

| Status | Meaning |
| --- | --- |
| `no-such-profile` | TikTok returned no profile for the username (code 10221). TikTok gives the same answer for a name that never existed, one that was changed and one that is no longer available; the row does not claim which. |
| `private-account` | The account is private, so its videos were not requested. |
| `list-refused` | TikTok refused the video list (code 10201). It gives the same answer for a secUid that does not exist and for a malformed request. |
| `no-videos-listed` | TikTok's video list for the account is empty (the row adds the profile's video count). |
| `no-videos-in-date-range` | The account has videos, but none inside `postedAfter` / `postedBefore`. |
| `verification-page` | TikTok answered with a verification page (a check that the visitor is a person). The Actor does not try to get past it and does not retry; the run stops there. |
| `blocked` | TikTok refused the request (HTTP 403, not retried) or kept answering with a rate limit or an empty response after retrying. |
| `unreadable` | The page could not be read (`tiktokStatusCode` has TikTok's code when it gave one). |
| `stopped-reading` | Reading an account stopped part way (a later page could not be read). The rows returned before that are complete. |
| `max-videos-reached` | The account reached `maxVideosPerProfile` videos. |
| `invalid-input` | The entry is not a username, profile URL or secUid, it is a short link, or a date could not be read. |
| `duplicate` | The same account appeared earlier in the list. |
| `no-new-videos` | Monitoring mode: no new videos on the accounts checked. |
| `budget-reached` | The run's maximum total charge had no room for more rows (the row says how many videos or accounts were left out, or that TikTok lists more videos that were not read). |
| `not-checked` | More than 200 accounts were given, or the run stopped at a verification page before reaching them. |

### Pricing

- **$4.00 per 1,000 videos** returned, charged only for rows with `status: "ok"`.
- In monitoring mode, also **$0.30 per 1,000 account checks**: every account checked costs one check, whether or not it has new videos. Example: 100 accounts checked every hour is 72,000 checks a month, which is **$21.60**, plus $4.00 per 1,000 new videos returned.

The Actor reads only as many pages as the run's maximum total charge can pay for (in monitoring mode, an account check and a video row each), and a free row says what was left out.

### Monitoring mode

- The first run of an account returns up to `maxVideosPerProfile` of its newest videos with `isFirstCheck: true` and remembers the time of that check.
- Later runs read the account's list from the newest video down to 3 days before the previous check and return only videos posted after it that were not returned before, with `isNew: true`. They are not cut by `maxVideosPerProfile`. A check of an account that posts a few videos a day reads one page (15 videos); on 2026-09-29 a second check of @nasa and @duolingo read one page each.
- To catch videos that appear in TikTok's list a little late, the boundary is placed 6 hours before each check, and videos already returned inside that window are remembered so they are not returned twice.
- If a run stops part way (for example at the maximum total charge), the boundary does not move, so videos that were not reached are returned by the next run.
- Videos posted before the first check and not returned by it (beyond `maxVideosPerProfile`) are never returned by later checks.
- The memory is kept per account (by its secUid, so a renamed account keeps its memory), not per list. **Do not put the same account in two schedules that can run at the same time**: Apify's key-value store has no atomic update, so two runs of the same account finishing together can overwrite each other.
- If Apify restarts or moves a run, it continues after the accounts it had already finished, and a video already in the run's dataset is not returned or charged again.

### Limits

- Up to 200 accounts per run and 10,000 videos per account, read one page (15 videos) at a time with a short pause between requests.
- The Actor does not download video files, does not read comments and does not search TikTok; give it the accounts.
- It does not follow short links (`vm.tiktok.com`); open one in a browser and paste the profile URL.

# Actor input Schema

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

Accounts to list the videos of: a username (nasa or @nasa), a profile URL (https://www.tiktok.com/@nasa, with or without the scheme; a video URL of the account works too) or a secUid (MS4wLjABAAAA...). One per line (commas and spaces also separate entries). Short links (vm.tiktok.com) are not followed: open one in a browser and paste the profile URL. A repeated account is read and charged once and the repeat gets a free 'duplicate' row. If you leave this field out with monitoring off, one example account is read; with monitoring on it is required. An empty list is rejected.

## `maxVideosPerProfile` (type: `integer`):

With monitoring off, reading an account stops once this many charged video rows have been returned for it, newest first, and a free row says so. In monitoring mode it limits only the first check of an account; later checks return every new video found, as far as the run's maximum total charge allows.

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

Optional. A date (2026-09-01, read as 00:00 UTC), a time (2026-09-01T12:00:00Z) or a number of days back (30 days). Older videos are not returned or charged, and reading an account stops once it reaches videos 3 days older than this (TikTok's list is not strictly in time order).

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

Optional. Same formats as postedAfter. Reading starts at this time instead of at the newest video, so older periods can be exported without paying for newer videos. Not available in monitoring mode.

## `monitoringMode` (type: `boolean`):

Off = every run returns the account's videos, charged per row. On = the first run of an account returns up to maxVideosPerProfile videos and remembers the time of that check; later runs return only videos posted after the previous check that were not returned before. **In monitoring mode every account checked costs $0.30 per 1,000 checks, whether or not it has new videos**, plus the row price for the videos returned. Example: 100 accounts checked every hour = 72,000 checks a month = $21.60. The Actor checks only as many accounts as the run's maximum total charge can pay for with a check and a video row each. Remembered per account; do not put the same account in two schedules that can run at the same time.

## `resetMonitoringState` (type: `boolean`):

Clears everything remembered for every account, so the next monitoring run treats each account as a first check again. This affects all your monitoring runs, because the memory is stored per account rather than per list. Turn it off again after one run: left on in a schedule, every run is a first check and charges for up to maxVideosPerProfile videos per account.

## `useProxy` (type: `boolean`):

Every page is requested directly first. Only if TikTok answers a page with a rate limit (HTTP 429 or 503), an empty response or no response is that page retried through a proxy session located in the United States; the next page is requested directly again. A verification page is never retried: the run stops there. With it off, a rate limit is retried directly and then returned as a free 'blocked' row, and no proxy is paid for.

## Actor input object example

```json
{
  "profiles": [
    "https://www.tiktok.com/@nasa"
  ],
  "maxVideosPerProfile": 100,
  "monitoringMode": false,
  "resetMonitoringState": false,
  "useProxy": true
}
```

# Actor output Schema

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

One row per video (or photo post) of each TikTok account: the video id and URL, the caption, the time it was posted, the view, like, comment, share and save counts, the length, the sound, the hashtags and mentions, whether it is pinned, TikTok's isAd flag and the tagged place, with the account's video and follower counts. In monitoring mode only videos posted since the last check are returned. Accounts that do not exist, private accounts, verification pages and invalid input come back as free rows that say why.

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

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

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/tiktok-profile-videos-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/IrzTWyqoPrB2Gk9X8/builds/90DlDORJqz1fd6ObP/openapi.json
