# TikTok Hashtag Views Scraper - Sounds, Profiles, Video Stats (`snow_leo_data/tiktok-scraper`) Actor

68 columns per video, 59 filled on average, and 40 per profile, measured on live accounts with no proxy and no login. TikTok video metrics, TikTok hashtag views and captions to filter on; watch mode returns only new or changed videos, and unchanged items are not charged.

- **URL**: https://apify.com/snow\_leo\_data/tiktok-scraper.md
- **Developed by:** [Snow Leo Data](https://apify.com/snow_leo_data) (community)
- **Categories:** Social media, For creators, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.49 / 1,000 result rows

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 Scraper — Profiles, Videos, Hashtags

Scrape TikTok profiles, videos, hashtags and sounds straight from the public
pages. No login, no cookies, no proxies, no browser — a plain HTTP client and
the Python standard library.

Every number below was measured on 2026-09-17 and can be reproduced with the
commands at the bottom of this page.

### What you get

**Profile rows — 40 columns.** Handle, display name, bio, bio link, avatar,
verification, private flag, followers, following, total likes, video count,
diggs, friends, account creation date, secUid, language, seller and commerce
flags, comment / duet / stitch / download settings, following visibility.
On live profiles 33 of the 40 columns come back filled.

**Video rows — 68 columns, 59 filled on average.** Views, likes, comments,
shares, saves and reposts; caption, hashtags, mentions, publication time,
duration; music id, title, author, duration, copyright and original flags;
cover, dynamic cover and download links; width, height, definition, format,
codec, bitrate, file size and TikTok's own video quality score; content
categories, caption language, country of creation, AI-generated flag,
slideshow images, duet / stitch / share / comment permissions.

**Hashtag rows.** The hashtag's own totals — total views and total video count
— which the public hashtag page itself does not show anywhere. Measured:
`#funny` 8 645 247 703 414 views over 341 846 664 videos, `#fyp`
128 091 069 010 621 views over 9 291 323 819 videos, `#cooking`
548 013 780 675 views over 18 922 488 videos.

**Sound rows.** Sound id, title, author and how many videos use it.

### What it costs to run

Two measured runs, both at 1024 MB, five profiles and three hashtags:

| mode | rows | time | platform cost | per 1 000 rows |
|---|---|---|---|---|
| full video metrics | 98 | 15.9 s | $0.001554 | **$0.0159** |
| views only (`includeVideoDetails: false`) | 98 | 4.7 s | $0.000867 | **$0.0088** |

The full-metrics run made 103 requests and downloaded 8.5 MB. Requests are
sent with `Accept-Encoding: gzip`, which is not a detail: the profile page is
370 091 bytes uncompressed and 84 941 compressed, the video page 405 896
against 89 333 — 4.36 times less traffic for the same data.

### The one limit you should know before you buy

**TikTok publishes 10 to 13 videos per creator without a signature, and this
actor returns those.** Measured on eight accounts: nasa 11, tiktok 10,
khaby.lame 10, zachking 12, bbc 13, nba 13, gordonramsayofficial 12,
duolingo 11. A hashtag gives 10 videos, a sound gives 6.

This is a wall in the source, not a setting. `count`, `cursor`, `page`,
`offset` and the path form `/page/N` were all tried: the response is
byte-identical, and the union over eight pages is still the same 11 videos.
The signed endpoint that would return more (`tiktok.com/api/post/item_list/`)
answers **HTTP 200 with exactly zero bytes** without a browser-generated
signature — a polite refusal that looks like success, and this actor treats it
as the failure it is.

If you need the full back catalogue of a creator, you need a browser-based
scraper with residential proxies, and it will cost you accordingly. If you
need current numbers for accounts, recent videos, hashtags and sounds, this
actor does it without either.

### Watch mode

`onlyNewOrChanged` remembers what earlier runs delivered in a named key-value
store and returns only what is new or what moved, marked `NEW` or `UPDATED` in
`change_type`. Unchanged items are not returned and not charged. Set
`emitUnchanged` to see them anyway.

Two honest notes. Cover and file links are signed afresh on every request, so
they are deliberately left out of the change fingerprint — otherwise every
video would look updated every run. And hashtag totals move continuously: two
requests seconds apart returned different view counts for `#cooking`, so
hashtag rows will nearly always be `UPDATED`.

### Filters, and which of them are free

`minPlayCount`, `postedAfter`, `postedBefore` and `textContains` work without
opening a single video page: views, caption and the publication time all come
from the creator card, the last one read out of the video ID itself.

`minDiggCount` and `minCommentCount` exist only on the video page. Asking for
them with `includeVideoDetails: false` would otherwise return zero rows, so the
run switches details on by itself and says so in the log and in the report
field `details_forced_by`.

Publication time from the video ID is a day-level estimate, and rows that use
it carry `created_at_is_estimated: true`. Measured against the exact value on
nine videos: six matched within 25 seconds, three drifted by 37 minutes,
86 minutes and 18.5 hours — scheduled posts. With full video metrics on, the
exact time from the page is always used.

A filter never drops a row whose field is empty. Missing data is not a
mismatch.

### Empty results always name their cause

A green run with an empty dataset is the worst kind of failure, so this actor
never stays silent about one. The report record `REPORT` carries
`empty_reason`, and the log carries the same sentence in words:

| `empty_reason` | what happened |
|---|---|
| `videos-disabled-but-only-videos-wanted` | `outputTypes` asks for videos while `includeVideos` is off |
| `videos-disabled-but-no-other-source` | only profiles given, videos off, profile rows not requested |
| `charge-limit-reached` | the run's spending limit was already used up |
| `filtered-out` | rows were collected and your filters removed all of them |
| `nothing-changed` | watch mode: everything had already been delivered |
| `source-refused` | TikTok returned nothing usable for any target |
| `source-returned-nothing` | pages opened but held no profiles, videos or hashtags |

The first two are decided from the input alone, before a single request is
sent, so a contradictory input costs you nothing.

A run with no targets at all is not an error: it scrapes the demo profile
`@nasa`, says so in the log and records `demo_target` in the report, so a
scheduled run never looks broken just because a field was left blank.

### Input

| field | meaning |
|---|---|
| `profiles` | handles or profile URLs: `nasa`, `@nasa`, `https://www.tiktok.com/@nasa` |
| `videoUrls` | direct video links or bare IDs; always returned fully detailed |
| `hashtags` | hashtags without `#` |
| `sounds` | sound IDs or music URLs |
| `maxItems` | hard cap on rows written; limits the output, not the crawl |
| `maxVideosPerSource` | quota per target, so one account cannot eat the whole run |
| `includeVideos` | collect videos at all |
| `includeVideoDetails` | open each video page for the full metrics |
| `outputTypes` | which of profile / video / hashtag / sound rows to keep |
| `minPlayCount`, `minDiggCount`, `minCommentCount` | engagement floors |
| `postedAfter`, `postedBefore`, `textContains` | date and caption filters |
| `onlyNewOrChanged`, `emitUnchanged` | watch mode |
| `compactOutput`, `excludeEmptyFields` | narrow output for AI agents and spreadsheets |
| `concurrency` | parallel page requests, 1 to 8 |

### What this actor does not do

Named plainly, because finding out after paying is worse than reading it here:

- **no keyword search.** `tiktok.com/search` carries no data in the page and
  `api/search/general/full` answers 200 with zero bytes without a signature.
- **no comments.** The comment endpoint is signed.
- **no follower or following lists.** Same reason.
- **no video, image or subtitle file downloads**, no transcripts, no AI
  descriptions. Cover and download links are in the output; fetching the media
  is not.
- **no full back catalogue per creator** — see the limit section above.
- **no `isPinned`, `isSponsored`, `isStory` or place-of-posting fields.** They
  are not on the public pages this actor reads.

### Reproducing the numbers

```bash
python3 tests/test_logic.py       # 60 checks on payloads captured from live pages
python3 tools/run_live.py         # the live suite, executed in the Apify cloud
python3 tests/test_lifecycle.py   # interruption, repeat runs, memory, spend limit
python3 tools/verify_readme.py    # every number in this file against the code
python3 tools/prove.py            # breaks the code on purpose; the suites must fail
```

The live suite runs in the cloud on purpose. From a home address
`tiktok.com/@user` returns a 1 462-byte stub; from Apify it returns 370 273
bytes with the data. A local "success" here would be a lie.

# Actor input Schema

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

Usernames or profile URLs: nasa, @nasa or https://www.tiktok.com/@nasa. Each profile returns one profile row with follower, like and video counts, plus the videos TikTok publishes on the public creator card (10-13 per profile, measured on eight accounts).

## `videoUrls` (type: `array`):

Direct links to single videos, for example https://www.tiktok.com/@tiktok/video/7106594312292453675. Bare video IDs work too. These always come back fully detailed.

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

Hashtags without the # sign, for example funny. Returns the hashtag's own total views and total videos plus 10 videos carrying it.

## `sounds` (type: `array`):

TikTok sound IDs or music URLs. Returns the sound and up to 6 videos using it.

## `maxItems` (type: `integer`):

Hard cap on rows written to the dataset, so you never pay for more than you asked for. It limits the OUTPUT, not the crawl. Leave empty for a 50-row trial run.

## `maxVideosPerSource` (type: `integer`):

Quota per target, so one big account cannot eat the whole run. 0 means every video the public page offers.

## `includeVideos` (type: `boolean`):

Switch off to get only profile, hashtag and sound rows and skip every video page.

## `includeVideoDetails` (type: `boolean`):

Opens each video page for likes, comments, shares, saves, reposts, music, hashtags, mentions and video technicals. Off, a video costs about 4 KB of traffic and carries views only; on, about 89 KB and carries all 60 fields.

## `outputTypes` (type: `array`):

Which kinds of rows land in the dataset. Empty means all four.

## `minPlayCount` (type: `integer`):

Keep videos with at least this many views. Works even with full metrics off, because the view count is on the creator card already.

## `minDiggCount` (type: `integer`):

Keep videos with at least this many likes. Likes only exist on the video page, so this switches full video metrics on for the run and says so in the log.

## `minCommentCount` (type: `integer`):

Keep videos with at least this many comments. Also needs full video metrics and switches them on.

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

Keep videos published on or after this date, YYYY-MM-DD. With full video metrics on, the exact publication time from the video page is used. With them off, the time is read out of the video ID: measured against the page on nine videos, six matched within 25 seconds and three drifted by 37 minutes to 18.5 hours, so treat it as day-level accuracy.

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

Keep videos published on or before this date, YYYY-MM-DD.

## `textContains` (type: `string`):

Keep videos whose caption or hashtags contain this text. Case is ignored.

## `onlyNewOrChanged` (type: `boolean`):

Remembers what earlier runs delivered and returns only items that are new or whose numbers moved, marked NEW or UPDATED in change\_type. Built for scheduled monitoring: unchanged items are not returned and not charged.

## `emitUnchanged` (type: `boolean`):

In watch mode, return unchanged items too, marked UNCHANGED. They are charged like any other row.

## `compactOutput` (type: `boolean`):

Only the columns that carry meaning, for AI agents and spreadsheets.

## `excludeEmptyFields` (type: `boolean`):

Leave empty columns out of every row instead of writing nulls.

## `concurrency` (type: `integer`):

How many TikTok pages to open at once. Lower it if you see rate limiting.

## Actor input object example

```json
{
  "profiles": [
    "nasa"
  ],
  "maxItems": 0,
  "maxVideosPerSource": 0,
  "includeVideos": true,
  "includeVideoDetails": true,
  "outputTypes": [
    "profile",
    "video",
    "hashtag",
    "sound"
  ],
  "minPlayCount": 0,
  "minDiggCount": 0,
  "minCommentCount": 0,
  "onlyNewOrChanged": false,
  "emitUnchanged": false,
  "compactOutput": false,
  "excludeEmptyFields": false,
  "concurrency": 4
}
```

# Actor output Schema

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

All collected rows

# 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"
    ]
};

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

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,snow_leo_data/tiktok-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/gOBLdlGPpCKpER8Zh/builds/ozQhP4VbATsd0bc4z/openapi.json
