# TikTok Video Views Tracker & Monitor - Views, Likes, Shares (`neverempty/tiktok-video-views-monitor`) Actor

Track TikTok video views, likes, comments, shares and saves from video URLs, with engagement rate and the rounding TikTok applies. Monitoring mode returns a video only when its view count changes, with each change. Deleted and private videos come back free with the reason.

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

## Pricing

from $2.56 / 1,000 video row 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 Video Views Tracker & Monitor - Views, Likes, Shares

Track the views, likes, comments, shares and saves of TikTok videos from their URLs. Each row also gives the **engagement rate**, the **author's follower count** and, for every count, **the step TikTok rounded it to** - so you know that `32300000` views is TikTok's "32.3M", not an exact number.

Turn on **monitoring mode**, schedule the Actor, and a run returns a video **only when its view count changed** since the row last returned for it (or when any count changed, if you choose), with each change. Deleted, private and non-existent videos come back as free rows that say which of these TikTok reports.

### What you can use it for

- **Campaign and influencer reporting**: paste the videos of a campaign and export views, likes, comments, shares, saves and engagement rate to a spreadsheet or dashboard.
- **View tracking over time**: schedule monitoring mode and receive a row each time a video's view count moves, without paying the row price for videos whose view count did not move.
- **Sponsored-post checks**: find out when a paid video was deleted or made private - the run tells you in a free row, with the last view count it returned.
- **Competitor and trend research**: compare engagement across creators with TikTok's rounding made explicit instead of hidden.

### How it works

1. Paste video URLs (`https://www.tiktok.com/@name/video/1234567890123456789`), bare numeric video ids, or `vm.tiktok.com` short links.
2. For each video the Actor opens the public video page and checks that TikTok answered with **the same video id you asked for**.
3. It reads the counts TikTok puts on that page and returns one row per video.
4. Videos TikTok does not show get a free row with TikTok's own reason.

### Input

| Field | Default | What it does |
| --- | --- | --- |
| `videos` | example videos | Video URLs, numeric video ids or short links, one per line (commas also separate entries, and so do spaces between entries that are all video URLs or ids). A repeated video is read and charged once; the repeat gets a free `duplicate` row. |
| `maxVideos` | `100` | With monitoring off, stop after this many rows. In monitoring mode it does not cut changes. Up to 1,000 entries per run. |
| `monitoringMode` | `false` | Remember each video's counts and return a video only when its view count (or, with `changeOn`, any count) changes. |
| `changeOn` | `views` | In monitoring mode: `views` returns a video when its shown view count changed; `any-count` when any of the five counts changed. |
| `resetMonitoringState` | `false` | Forget every remembered count and start a fresh baseline. Turn it off again after one run. |
| `useProxy` | `true` | Retry a page through a proxy only if TikTok answers it with a bot check. |

```json
{
    "videos": [
        "https://www.tiktok.com/@mrbeast/video/7654638524729216287",
        "https://www.tiktok.com/@zachking/video/6768504823336815877"
    ],
    "monitoringMode": true
}
```

If you leave `videos` out of the input with monitoring off, the two example videos above are read (their URLs appear in `inputUrl` and the run log says so). In monitoring mode `videos` is required, so a schedule never pays to watch the examples. An empty list is rejected with a free `invalid-input` row instead of reading the examples.

### Output

One row per video, shortened. The counts were read from Apify on 2026-09-14 (UTC); the monitoring fields show what a later run returns after the view count moves from 32.2M to 32.3M:

```json
{
    "status": "ok",
    "videoId": "7654638524729216287",
    "videoUrl": "https://www.tiktok.com/@mrbeast/video/7654638524729216287",
    "authorUsername": "mrbeast",
    "authorFollowerCount": 140700000,
    "createdAt": "2026-06-23T17:07:14.000Z",
    "playCount": 32300000,
    "likeCount": 2900000,
    "commentCount": 48100,
    "shareCount": 35500,
    "saveCount": 118865,
    "playCountRoundingUnit": 100000,
    "likeCountRoundingUnit": 100000,
    "commentCountRoundingUnit": 100,
    "shareCountRoundingUnit": 100,
    "saveCountRoundingUnit": 1,
    "countsMayBeRounded": true,
    "engagementRate": 0.092372,
    "engagementRateAtLeast": 0.088993,
    "engagementRateAtMost": 0.095771,
    "change": "counts-changed",
    "previousCheckedAt": "2026-09-15T07:00:00.000Z",
    "previousCountsReadAt": "2026-09-15T06:00:00.000Z",
    "hoursSincePreviousCounts": 2.5,
    "previousPlayCount": 32200000,
    "playCountDelta": 100000,
    "playCountDeltaAtLeast": -99998,
    "playCountDeltaAtMost": 299998
}
```

| Field | Meaning |
| --- | --- |
| `playCount`, `likeCount`, `commentCount`, `shareCount`, `saveCount` | The counts TikTok puts on the video page, as numbers. |
| `<count>RoundingUnit` | The step TikTok rounded that count to: `1` means not rounded, `100` means shown like "48.1K", `100000` like "32.3M", `100000000` like "2.4B". |
| `countsMayBeRounded` | `true` when any of the five counts has a rounding unit above 1. |
| `engagementRate` | (likes + comments + shares) / views, from the counts as shown. Saves are not included. |
| `engagementRateAtLeast`, `engagementRateAtMost` | The lowest and highest the engagement rate could be if the only error in the counts were TikTok's rounding. When no count is rounded they differ from the rate by at most 0.000001. See "Counts differ between requests" below. |
| `authorFollowerCount`, `authorFollowerCountRoundingUnit` | The author's follower count on the video page, and its rounding step. |
| `authorMatchesInput` | `false` when the URL you gave has a different username than the video's author. TikTok serves the video by id whatever name is in the URL, so the id decides which video you get. |
| `isAd` | TikTok's own `isAd` flag for the video. |
| `resolvedFromShortLink` | The short link you gave, when the video was reached through one. |
| `change` and the `previous…` / `…Delta` fields | Monitoring only; see below. `null` when monitoring is off. |

`description` is passed through as TikTok shows it, except that e-mail addresses and phone-number-like digit runs in it are replaced with `[email removed]` / `[phone removed]`. Account names, ids and URLs are never altered.

#### About the counts and the rounding

TikTok's video page carries the counts the way the app displays them. On the 197 videos with counts read from a home connection on 2026-09-14 (UTC):

- **All 515 view, like, comment and share counts of 10,000 or more were rounded**: to a step of 100 below one million (`981.4K` comes back as `981400`), to a step of 100,000 from one million (`32.3M` as `32300000`), and to a step of 100,000,000 from one billion (`2.4B` as `2400000000`). Counts below 10,000 were not rounded (`1142`).
- **None of the 110 save counts of 10,000 or more were rounded** (`118865`, `984228`).

The rounding unit is worked out from the size of the number: a count that is not a multiple of the step for its size is not rounded, so its unit is `1`. A save count that happens to be a round multiple of 100 is given a unit of 100. Whether TikTok rounds down or to the nearest step has not been measured, so the ranges allow one full rounding step in either direction.

**Very large videos**: TikTok's older count field overflows above about 2.1 billion. For the Zach King video in the example, that field says `-1894967296` views while the current field says `2400000000`. This Actor reads the current field and never returns a negative count.

`repostCount` is not returned: it was `0` on all 197 videos measured, so its meaning could not be checked.

#### Counts differ between requests

The same video can come back with different counts depending on when and from where it is requested. On 2026-09-14 (UTC), one video's comment count was 282,000 when read from Apify and 263,100 when read from another connection 12 seconds later, and the Zach King video's comment count read from Apify went from 781,900 to 790,900 within 20 minutes. The `…AtLeast` / `…AtMost` ranges only account for TikTok's rounding, not for these differences, so treat them as a guide to how much rounding can hide, not as a guarantee.

### Monitoring, and what it costs

Turn **monitoring mode** on and the Actor remembers each video's counts. On later runs a video comes back only with one of these `change` values:

| `change` | Meaning |
|---|---|
| `first-check` | The video had not been seen before. Every video is returned once on the first run, to set the baseline. |
| `counts-changed` | With `changeOn: "views"` (the default): the shown view count differs from the row last returned for the video. With `changeOn: "any-count"`: any of the five shown counts differs. |

A video that is not returned **keeps the counts of the row last returned for it**, so every changed row carries the change since the previous row you received: `previous<Count>` (for example `previousPlayCount`), `<count>Delta` (the difference between the shown numbers) and `<count>DeltaAtLeast` / `<count>DeltaAtMost` (the range of that change allowing for TikTok's rounding of both numbers). It also carries `previousCountsReadAt` (when the previous row's counts were read), `hoursSincePreviousCounts`, and `previousCheckedAt` (the last run that read the video, returned or not).

**Why `views` is the default**: save counts are not rounded and keep moving on videos that are still being watched. When 4 videos were re-checked from Apify 20 minutes apart on 2026-09-14 (UTC), 3 had new save counts, 2 of them with an unchanged view count. With `changeOn: "any-count"` such a video can come back, and be charged the row price, on every run.

**Read the range, not only the delta, for large videos.** A video at 32.2M that shows 32.3M later has a `playCountDelta` of 100,000, while its range allowing for rounding runs from -99,998 to 299,998, because both numbers are rounded to 100,000. For the same reason a large video can gain views without its shown view count changing, and then it is not returned.

A video that is not returned costs no row price; the check fee for reading it still applies. When nothing is returned at all, the run returns a single free `no-change` row. If the run's maximum total charge stops a changed row from being delivered, the new counts are not remembered, so the video still counts as changed the next time it is checked.

Counts are remembered **per video, not per list**, so adding a video does not reset the others. They are saved after every 10 entries of the list, when the run ends, and when Apify signals that the run is being aborted or migrated (the run then stops reading further videos). If Apify restarts a run - for example after migrating it to another server - the run continues after the videos it had already handled instead of reading and charging them again. When Apify announces a migration, the run saves its place, finishes the video it is on, saves again and waits to be moved, so the videos handled before it are not read again. A restart without warning continues from the last save, so up to the last 10 entries handled before it can be read, returned and charged once more, and the run log's count does not include those repeats (3 videos were repeated in a test restart on 2026-09-14 UTC). In monitoring mode `maxVideos` does not cut changes: every video in the list is checked, as far as the run's maximum total charge allows. A video that was returned before and is now deleted or private comes back as a free row with `lastSeenPlayCount` and `lastSeenAt`; its remembered counts are kept, so if it comes back, it is compared with them.

**Do not put the same video in two schedules that can run at the same time.** Each run writes back only the videos it checked, merged into the counts as they are at that moment, then reads them again and writes once more if another run overwrote them. Apify's key-value store has no atomic update, so this cannot be made watertight: runs that finish at the same instant, or check the same video at the same time, can still overwrite each other's record, and a later run may return a `first-check` or a change a second time. For the same reason, turn `resetMonitoringState` off again after one run - left on in a schedule, every run returns and charges every video as a first check.

#### What monitoring costs

- **$0.30 per 1,000 video checks** - charged in monitoring mode for every video whose counts were read, whether or not they changed. Deleted, private and unreadable videos and bot checks are not charged.
- **The row price** - charged only for the rows returned, which in monitoring mode means `first-check` rows and changes.

Example: 50 videos checked every hour is 36,000 checks a month, which is **$10.80** in check fees, plus the rows for the videos whose view counts changed.

The Actor reads only as many videos as the run's maximum total charge can pay for with both a check and a changed row each, so a low limit cannot use up the budget on check fees and leave nothing for the rows. Videos beyond that are not read; a `budget-reached` row says how many, and raising the maximum total charge lets them be read. Runs with monitoring off are charged per row only.

### Rows that are never charged

When there is nothing to return for a video, this Actor says why instead of returning an empty or zero-filled row. TikTok answers every one of the first five with HTTP 200 and its own code and reason text, which are passed on in `tiktokStatusCode` and `tiktokStatusMessage`. Of 150 older videos listed in TikTok's own sitemap and read on 2026-09-14 (UTC), 31 came back as one of them:

| `status` | When |
| --- | --- |
| `video-deleted` | TikTok marks the video as deleted (code `10204`, `status_deleted`). |
| `video-private` | TikTok marks the video as visible only to its author (code `10204`, `status_self_see`, as TikTok labels it). |
| `private-account` | The author's account is private (code `10222`, `author_secret`; the profile page of such an account also reports it as private). |
| `no-such-video` | TikTok returns no video for the id (code `10204`, "item doesn't exist"). A made-up id gets exactly this answer, so the row does not claim the video was deleted. |
| `video-unavailable` | TikTok returns no video with another reason text, such as `status_audit_not_pass`, `status_reviewing` or `author_status`. The meaning of these has not been measured, so the text is passed through untranslated. |
| `no-counts-returned` | TikTok answered with the video id but without one or more of its counts (2 of 199 such pages had no counts, author or creation time at all). |
| `different-video-returned` | TikTok answered with a different video id than the one asked for. |
| `short-link-not-a-video` | The short link does not redirect to a video page. |
| `blocked` | TikTok answered with a bot check or an empty page on every try. The video is not reported as unavailable. |
| `unreadable` | The page could not be read, or TikTok answered with a code whose meaning has not been measured. |
| `invalid-input` | The entry is not a TikTok video URL, id or short link, `videos` is an empty list or missing in monitoring mode, or `changeOn` is not one of its two values. |
| `duplicate` | The same video was already handled by an earlier entry in this run. |
| `no-change` | Monitoring ran and no video was returned because none changed. |
| `not-checked` | `maxVideos` rows were already returned (monitoring off), or the list is longer than 1,000 entries. The row says how many entries were not read. |
| `budget-reached` | The run's maximum total charge has no room for another row (or, in monitoring mode, another check together with a changed row). The row says how many entries were not read. |

### Pricing

**$3.50 per 1,000 video rows**, charged only for rows with `status: "ok"`. In monitoring mode, **$0.30 per 1,000 video checks** as well (see above). Free rows are never charged. If you set a maximum total charge for the run, the Actor stops reading videos when no further row fits, and says so in a `budget-reached` row.

### Limits and notes

- Only public counts on the video page are returned. The Actor does not log in and does not read private videos.
- Photo posts are requested through the same video address; a public photo post has not been measured.
- Short links are followed one redirect. The Actor only uses the target when it is a TikTok video address it accepts as input.
- Video pages are requested one after another with a short pause between them. A page is retried through a proxy only when TikTok answers that page with a bot check; the next page is requested directly again.
- `engagementRate` uses shown (rounded) counts; `engagementRateAtLeast` and `engagementRateAtMost` show how much rounding alone can move it.

### FAQ

**Do I need a TikTok account or API key?** No. The Actor reads public video pages.

**Are the view counts exact?** No. View, like, comment and share counts of 10,000 and above are rounded by TikTok itself, and every row gives the rounding step. Counts can also differ between requests made moments apart. Save counts were not rounded in any video measured.

**Can I get the list of videos of an account?** No. Paste the video URLs you want to track.

**How often should I run monitoring?** As often as you want a new data point: each run costs the check fee per video, and a row only when the view count moved. Below 10,000 views every view shows; from 10,000 the shown count moves in steps of 100, and from one million in steps of 100,000.

# Actor input Schema

## `videos` (type: `array`):

Videos to read: https://www.tiktok.com/@name/video/1234567890123456789 (with or without the scheme), the numeric video id, or a vm.tiktok.com / vt.tiktok.com / tiktok.com/t/ short link. A repeated video is read and charged once and the repeat gets a free 'duplicate' row. If you leave this field out with monitoring off, two example videos are read; with monitoring on it is required. An empty list is rejected.

## `maxVideos` (type: `integer`):

With monitoring off, reading stops once this many charged rows have been returned, and a free row says how many entries at the end of the list were not read. Deleted, private and unreadable videos do not count. In monitoring mode it does not limit the changes returned: every video in the list is checked, as far as the run's maximum total charge allows. Up to 1,000 entries are read per run.

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

Off = every video comes back with its current counts, charged per row. On = the Actor remembers each video's counts and, on later runs, returns a video only when its shown view count changed since the row last returned for it (or any count, with changeOn), with each change and the range it could be in given TikTok's rounding. The first run returns every video once to set the baseline. **In monitoring mode every video whose counts are read costs $0.30 per 1,000 checks, changed or not** (deleted, private and unreadable videos and bot checks are free), plus the row price for the rows returned. Example: 50 videos every hour = 36,000 checks a month = $10.80. The Actor reads only as many videos as the run's maximum total charge can pay for with a check and a change row each. Counts are remembered per video; do not put the same video in two schedules that can run at the same time.

## `changeOn` (type: `string`):

views = only when the shown view count changed (the default). any-count = when the shown view, like, comment, share or save count changed. Save counts are not rounded by TikTok, and 3 of 4 videos re-checked from Apify 20 minutes apart on 2026-09-14 UTC had new save counts, 2 of them with an unchanged view count, so with any-count a video that is still being watched can come back, and is charged the row price, on every run. A video that is not returned keeps the counts of the row last returned for it, so the next row's changes add up from there.

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

Clears every remembered count for this Actor, so the next monitoring run returns each video once again as a first check. This affects all your monitoring runs, because counts are stored per video rather than per list. Turn it off again after one run: left on in a schedule, every run returns every video as a first check and charges the row price for it.

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

Every video page is requested directly first. Only if TikTok answers a page with a bot check, an empty response or no response is that page retried through a proxy session; the next video is requested directly again. With it off, that answer is retried directly and then returned as a free 'blocked' row, and no proxy is paid for.

## Actor input object example

```json
{
  "videos": [
    "https://www.tiktok.com/@mrbeast/video/7654638524729216287",
    "https://www.tiktok.com/@zachking/video/6768504823336815877"
  ],
  "maxVideos": 100,
  "monitoringMode": false,
  "changeOn": "views",
  "resetMonitoringState": false,
  "useProxy": true
}
```

# Actor output Schema

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

One row per TikTok video whose counts were read: the video id and URL, the author and follower count, description, hashtags, creation time and duration, the view, like, comment, share and save counts as TikTok shows them, the rounding step of each count, the engagement rate with its range, and in monitoring mode the change in each count since the row last returned for the video, with the range TikTok's rounding allows. Deleted, private and non-existent videos, bot checks 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 = {
    "videos": [
        "https://www.tiktok.com/@mrbeast/video/7654638524729216287",
        "https://www.tiktok.com/@zachking/video/6768504823336815877"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/tiktok-video-views-monitor").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 = { "videos": [
        "https://www.tiktok.com/@mrbeast/video/7654638524729216287",
        "https://www.tiktok.com/@zachking/video/6768504823336815877",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/tiktok-video-views-monitor").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 '{
  "videos": [
    "https://www.tiktok.com/@mrbeast/video/7654638524729216287",
    "https://www.tiktok.com/@zachking/video/6768504823336815877"
  ]
}' |
apify call neverempty/tiktok-video-views-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/tiktok-video-views-monitor"
        }
    }
}
```

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/btqEeCghOxWmV0tou/builds/MOLV2AZgWaGjR4ybv/openapi.json
