TikTok Video Stats: Views, Likes, Comments, Shares by URL avatar

TikTok Video Stats: Views, Likes, Comments, Shares by URL

Pricing

Pay per usage

Go to Apify Store
TikTok Video Stats: Views, Likes, Comments, Shares by URL

TikTok Video Stats: Views, Likes, Comments, Shares by URL

Fresh views, likes, comments, shares and saves for a list of TikTok videos, read from the public video page. No login, no browser, no proxy. Deleted and private videos get their own status. Built for repeat measurements.

Pricing

Pay per usage

Rating

0.0

(0)

Developer

Sergey Faraday

Sergey Faraday

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

0

Monthly active users

6 hours ago

Last modified

Share

Measures known TikTok videos again and again: views, likes, comments, shares and saves, each time with the moment of measurement. Give it a list of video URLs and compare snapshots to see how each video grows.

It reads the public video page with plain HTTP requests. No login, no cookies, no browser and no proxy, so it costs a fraction of a browser-based scraper.

Not collected here: search results, hashtag and sound video lists. TikTok does not serve them openly. You can get them from the companion Unified Scraper Adapter actor (modules tiktok_search, tiktok_sound).

How to use

  1. Paste video URLs or ids into videoUrls. For a panel measured over time use numeric ids: resolve short links first, because a dead short link has no numeric id to join with earlier rows (sampling.requestedAs is the only link). requestedAs holds the form you asked in without its query string and without the author handle (https://www.tiktok.com/@_/video/<id>). sampling.requestedAs, data.requestedAs, applied.controlVideo.requestedAs and invalid inputs in sources[].excluded keep origin and path only: the query string and fragment of a share link are never stored, and a handle in the path of your URL is replaced by @_.
  2. Give each run a snapshotId (for example the date) and run it on a schedule. Always pass it: without it each run gets <actor>.<runId>, so a retry becomes a new snapshot.
  3. Compare snapshots by sourceRecordId, as in the next section. The actor does not compare by itself: one run cannot read the rows of another.

Comparing two snapshots

CaseHow to tell
newno earlier row for this sourceRecordId
unchangedsame provenance.contentChecksum. It covers saves, which are not rounded, so it also catches growth the rounded counters hide
changeddifferent provenance.contentChecksum
gonea deleted, not_public or not_found row where the earlier snapshot had a post. blocked, fetch_error and not_attempted rows say nothing about the video
below resolutiona difference smaller than precision.step: this is not zero growth, and a rounded counter can stay the same while the video grows

State rows take canonicalUrl from the id (https://www.tiktok.com/@_/video/<id>), so the form of the input does not read as a change. A short link stays its own key: before the read it cannot be tied to a video, so a short link against a full URL is another task. A change of the asked proxy country reads as changed through source.marketReason, not in the metrics; data.requestRegion names the country and stays out of the checksum.

Do not assume counters only grow: comments fell between readings in stored data, sometimes by more than the rounding step.

Input

FieldDefaultMeaning
videoUrlsvideo URLs, short links (vm.tiktok.com) or numeric video ids. 0, any short number and a short link host with no code in the path are invalid inputs: no request, listed in sources[].excluded with reason not_a_tiktok_video_reference, and the run status does not move
delayMs1500pause between two videos
stopAfterBlocks5stop after this many blocked videos in a row
controlVideoUrla video you know is live; see "Is not_found reliable?" below
snapshotId<actor>.<runId>your id for this measurement. One object has one row per snapshot: a measurement replaces a state row of the same video (their recordKeys differ by recordType, so join on sourceRecordId), a later run does not change a finished snapshot, and a value that differs is kept apart. A new id gives a new observation. Compare snapshots by provenance.contentChecksum
maxItems, maxRunSecondslimits. maxItems is a hard cap on rows: the ids past it get no row, they are listed in the MANIFEST under sources[].applied.notAttemptedIds and sources[].excluded with reason not_attempted:maxItems (excluded holds the first 200 ids, plus a count). A time cut comes back as not_attempted rows while maxItems allows. The maxRunSeconds budget counts from the run start and a restart does not renew it, the time between workers counts. A resurrected run past its budget stops at once and finishes partial; a new run under the same snapshotId within 24 hours completes the snapshot
{ "videoUrls": ["https://www.tiktok.com/@tiktok/video/7106594312292453675", "7534061113365859586"], "snapshotId": "2026-11-02" }

Output

One post row per live video:

{
"recordType": "post",
"platform": "tiktok",
"sourceRecordId": "7534061113365859586",
"canonicalUrl": "https://www.tiktok.com/@user/video/7534061113365859586",
"time": { "publishedAt": "2025-08-02T18:45:03.000Z", "observedAt": "2026-11-02T14:34:30.000Z", "observedAtBasis": "response_received" },
"metrics": {
"views": { "value": 87400, "sourceName": "statsV2.playCount", "unit": "count", "kind": "counter", "history": "live_snapshot", "precision": { "class": "platform_rounded", "step": 100, "rule": "tiktok.page.display.2026-11-02" } },
"likes": { "value": 6383, "sourceName": "statsV2.diggCount", "unit": "count", "kind": "counter", "history": "live_snapshot", "precision": { "class": "as_served", "step": 1, "rule": "tiktok.page.display.2026-11-02" } },
"comments": { "value": 33, "sourceName": "statsV2.commentCount" },
"shares": { "value": 787, "sourceName": "statsV2.shareCount" },
"saves": { "value": 381, "sourceName": "statsV2.collectCount" }
},
"data": { "text": "...", "hashtags": ["..."], "soundId": "...", "videoLocationCreated": "PT", "isAd": false, "authorPseudoId": "tiktok:a_...", "durationSec": 15, "pageStatusCode": 0, "pageServedRegion": "AE", "isPhotoPost": false, "privateItem": false, "secret": false }
}

A video that cannot be measured comes back as an object_state row with its own status and, when the page answered, the page's own code and message:

{
"recordType": "object_state",
"platform": "tiktok",
"sourceRecordId": "7042692929109986561",
"status": "deleted",
"statusReason": "TikTok page status 10204 (status_deleted)",
"data": { "requestedAs": "7042692929109986561", "pageStatusCode": 10204, "pageStatusMsg": "status_deleted", "pageServedRegion": "AE" },
"sampling": { "requestedAs": "7042692929109986561" }
}
statusMeaning
deletedTikTok's page names the video deleted
not_publicthe video exists or existed, but the page does not show it to the public
not_foundTikTok says there is no such video. This does not prove it ever existed; an id you saw live earlier reads as removed since then
region_blockedclosed for the region of the collector address
unavailablethe page answered something we cannot read: an unknown code or message, or another video than the one requested (data.errorCode is ID_MISMATCH)
blockedTikTok answered with a block or verification page: the video may well be alive. This also covers a 200 page without the data script or detail block that says captcha, verify you are human, access denied, unusual traffic or security check
fetch_errorour read failed: HTTP error, network, changed page layout. data.errorCode says which. This says nothing about the video. HTTP 503 without Retry-After counts here. With no response at all, observedAtBasis is request_failed: observedAt is when the read gave up
not_attemptedthe run stopped before this video; observedAt is null

Where the page codes get their meaning

TikTok documents none of these codes. Seen means we read that code and message on live pages on 2026-11-02. status_deleted and status_self_see were read from one address outside the Apify platform, so it is not established that a run on Apify gets the same messages. Community means a label from third-party open source projects: we never saw that code, and those projects partly disagree.

Code and messagestatusWhere the meaning comes from
10204 status_deleteddeletedseen; the meaning rests on the message itself
10204 status_self_seenot_publicseen; the message itself and one third-party patch
10204 item doesn't existnot_foundseen, also on ids that never existed
10204, any other messageunavailablecounted in counters.unclassified; the source stays partial
10216, 10217, 10221, 10222not_publiccommunity, never seen by us
10219, 10231region_blockedcommunity, never seen by us
10000, 10113, 10114blockedcommunity, never seen by us
any other codeunavailablecounted in counters.unclassified; the source stays partial

deleted, not_public, not_found and region_blocked are valid answers, so a run with only these and live videos is ok. A new code never reads as a valid answer until someone has looked at it.

The MANIFEST record in the key-value store reports the run status (ok, partial, blocked, failed), counts of measured, gone, region-blocked, blocked, failed, unclassified and not attempted videos, invalid inputs, cut videos and errors.

How to read the data

  • observedAt is read after the page response arrived, collectedAt when this actor built the row, so "what did we know at time T" uses collectedAt. runStartedAt is the platform start, the same on every row of a run. On a measured row runStartedAt <= observedAt <= collectedAt. The page states no date of its own for its counters, so sourceUpdatedAt is null and sourceUpdatedAtBasis says why.
  • Counters are the rounded numbers TikTok's page shows at observedAt: below 10,000 as they are, from 10,000 in steps of 100, from 1,000,000 in steps of 100,000, from 1,000,000,000 in steps of 100,000,000. The step is what was measured, not the direction of the rounding. Stored data holds views and likes at the 100,000 step; shares at that step and views at the 100,000,000 step rest on one live sample each; comments have no sample from 1,000,000. Saves are not rounded. The rule was measured on 2026-11-02 and is not documented by TikTok, so it can change.
  • Each metric carries precision with class, step and rule. platform_rounded: the true count is within about one step of the value. as_served: the page shows this number and no rounding is known. rule_mismatch: the value does not fit the rule, step is null, the value is untouched and the MANIFEST notes how many. exact is never written, and there are no low and high bounds, because the rounding direction is not established.
  • authorFollowers follows the same rule, authorFollowersStep gives its step.
  • Values come from statsV2 first and from stats per field only if statsV2 has no usable value: above 2^31 views stats.playCount wraps to a negative number. A positive value in stats beats a zero in statsV2. A counter in neither has sourceName absent:statsV2.x|stats.x.
  • pageServedRegion is what TikTok reports for the address that read the page: evidence for region effects, not a market and not an audience. requestRegion is the country asked of the Apify proxy (proxyConfiguration.apifyProxyCountry), on post and state rows, with requestRegionMethod; without one it is null with not_set. It says what was asked, pageServedRegion what TikTok reports; neither is a market or an audience. market is null because counters are global, and source.marketReason says so.
  • Author names are not stored as fields: authorPseudoId is a one-way hash. An original sound is titled by its author, so its title is not stored: data.soundTitle is null, data.soundIsOriginal is true and data.soundId stays. The handle appears in the video link, and the post text can hold whatever name its author wrote there. So the hash does not hide who posted.
  • data.hashtags holds one tag per video, in the spelling that came first: the challenge list first, then the text. 12 of 92 stored rows held one tag in two cases; those stored rows are not rewritten.
  • A value the page does not provide is null with a reason, never 0. Zero views on a live video are treated as not served. A video with 0 on every counter stays a post as served, views null and the others 0; nothing shows that block was served, so if views is unreadable or 0 on every post, or on 10% of 10 or more, the source is partial as described under a changed page.
  • An empty counter block, or one whose only readable value is a zero of views, is fetch_error with SCHEMA_CHANGED, not a post full of nulls.
  • A changed page. Every real page read so far sent playCount, diggCount, commentCount, shareCount, collectCount and createTime. If one of them has no readable value (absent, null, empty, not a number, or 0 for views) on every post of a run, from one post on, or on 10% or more of the posts when there are 10 or more, the source is partial with SCHEMA_CHANGED naming the field in errors[].field, with affected and of. counters.unreadableFields holds the count per field. The rows stay as they are: null with a reason, and unparseable with the raw value when the value is there but cannot be read. A real 0 of likes, comments, shares or saves is an answer and raises nothing.

FAQ

Are the numbers exact? No. They are what TikTok's own page shows, and views, likes, comments and shares are rounded as described above. Two readers of this page agree with each other because both read the same rounded numbers, so agreement between them does not show exactness. This page has no exact counts to give.

Is not_found reliable? It is what the page answered. We have seen no live video answer not_found, but other readers report 10204 also for an address that TikTok treats differently, so this is not excluded. Pass controlVideoUrl with a video you know is live: when some video answers not_found, the control is read as well, without a row of its own. If it does not read as live, the source is partial with the error NOT_FOUND_UNCONFIRMED, and sources[].applied.controlVideo in the MANIFEST shows what it answered.

What about photo posts? data.isPhotoPost is set when the page has a photo block, but no photo post has been checked on a live page yet.

What about search and hashtags? Those lists are not served openly. Use modules tiktok_search and tiktok_explore of the Unified Scraper Adapter.

Limits

  • If TikTok starts blocking the page, the run stops and says so. No bypass is attempted. A real block, a private account and a region block have not been seen on this actor yet, so those paths rest on the community labels above.
  • A migration or restart of the run is not a cut: the next worker goes on with the videos not yet read. A video whose row reached the dataset is counted from that row: it is not read again and gets no second row. A cut takes all the remaining videos at once, so it costs one push and a few writes however long the panel is. An aborted run names the videos left in sources[].excluded with reason not_attempted:aborting; the abort handler gives the collector up to 20 s to name them, and past the 200 listed in sources[].excluded the rest are in sources[].applied.notAttemptedIds. A full URL of a video that already came under a short link is not read again; measured is the number of videos with a post row.
  • The counters are the value at the moment of measurement. They cannot be spread over past dates.
  • Check that automated collection fits TikTok's terms and your use case.

Cost

About $0.03-0.10 of platform usage per 1,000 videos, depending on your Apify plan, at 256 MB of memory and the default delay. 1,000 videos take about 40 minutes.