# TikTok Video Scraper - Views, Likes & Hashtags (`eiv/tiktok-video-scraper`) Actor

TikTok video scraper that returns views, likes, comments, shares, saves and reposts for any list of video URLs, plus hashtags, sound, exact post date and the creator's follower count. No login, no API key, no browser.

- **URL**: https://apify.com/eiv/tiktok-video-scraper.md
- **Developed by:** [Eimantas V](https://apify.com/eiv) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 video scrapes

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?

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 Video Scraper — Views, Likes & Hashtags

TikTok video scraper that returns **views, likes, comments, shares, saves and
reposts** for any list of video URLs, plus hashtags, the sound, the exact post
date and the creator's follower count. No login, no API key, no browser.

Paste video URLs, get a spreadsheet of engagement. It is built for the job most
TikTok data actually gets bought for: measuring how a specific set of posts
performed — a campaign's deliverables, a competitor's last thirty uploads, a
hashtag's top posts you collected elsewhere.

### What you get

One row per video. Every field below was populated on a real captured video;
`null` means TikTok did not return it, never a stand-in zero.

| Field | Type | Notes |
|---|---|---|
| `playCount` | integer | null | Views |
| `likeCount` | integer | null | |
| `commentCount` | integer | null | |
| `shareCount` | integer | null | |
| `collectCount` | integer | null | Saves |
| `repostCount` | integer | null | Only present in TikTok's newer stats block |
| `engagementRate` | number | null | (likes+comments+shares) ÷ views, as a % |
| `description` | string | null | The caption |
| `createdAt` | string | null | **A real UTC timestamp**, not a relative string |
| `createdAtUnix` | integer | null | |
| `hashtags` | array | Tags the creator typed, in caption order and original casing |
| `challenges` | array | Every challenge TikTok associates — a superset of the above |
| `mentions` | array | @-handles in the caption |
| `musicTitle` / `musicAuthor` / `musicId` | string | null | The sound |
| `musicIsOriginal` | boolean | null | Original audio vs a licensed track |
| `durationSeconds` / `width` / `height` | integer | null | |
| `coverUrl` | string | null | |
| `authorHandle` / `authorName` / `authorId` | string | null | |
| `authorFollowerCount` | integer | null | The creator's followers **at scrape time** |
| `authorHeartCount` / `authorVideoCount` | integer | null | |
| `authorVerified` | boolean | null | |
| `authorSignature` | string | null | Creator bio |
| `isAd` | boolean | null | Branded content flag |
| `isAigc` | boolean | null | AI-generated content flag |
| `locationCreated` | string | null | Country code |
| `duetEnabled` / `stitchEnabled` | boolean | null | |
| `commentsDisabled` | boolean | null | |

#### Engagement rate is null when it cannot be computed

If views are missing or zero, `engagementRate` is `null` rather than `0` or
`Infinity`. A rate over an unknown denominator is not a measurement, and a
buyer sorting on it would otherwise be sorting on an artefact.

#### `hashtags` and `challenges` are different things, and are kept apart

TikTok exposes hashtags in two places that disagree, and the longer list is not
the better one.

`hashtags` is what the **creator actually typed**. It comes from the caption,
with character offsets that prove where each tag appears, so it preserves the
creator's own casing rather than TikTok's normalised form.

`challenges` is **TikTok's own association set**. It uses canonical casing and
it can include tags that appear nowhere in the caption. On the video used to
build this Actor the two differed twice over: the creator typed
`#petsoftiktok`, `challenges` reports it as `PetsOfTikTok`, and `challenges`
additionally carried `bonevoyage` — a tag the caption never contained. Merging
them would have made every row assert something false about the creator, so the
two stay separate.

Use `hashtags` for "what did they tag this with". Use `challenges` for reach and
discovery analysis, where TikTok's own associations are the point.

### Input

```json
{
  "videos": [
    "https://www.tiktok.com/@scout2015/video/6718335390845095173",
    "https://www.tiktok.com/@nasa/video/7016181555718065413",
    "6718335390845095173"
  ],
  "maxVideos": 1000,
  "requestDelayMs": 1500
}
```

Full video URLs, photo-post URLs, `/v/` and `/embed/v2/` links, and bare numeric
ids all work; extra query parameters are ignored. You can also chain from
another Actor's dataset with `sourceDatasetId`.

### Pricing

| Event | Price | When |
|---|---|---|
| `video-scraped` (primary) | **$1.00–$2.00 per 1,000** | Per video row written |
| `apify-actor-start` | $0.00005 | Once per run |

Your whole bill is `videos × row price + $0.00005`. One request produces one
row, so there is no second event and nothing to forecast around.

**Never charged:** error rows of any kind; a video TikTok reports as deleted or
non-existent; inputs that are not readable video URLs; and any request that came
back blocked and was retried. Full arithmetic and the measured cost basis are in
[docs/PRICING.md](docs/PRICING.md).

### What this does NOT do

Stated plainly, because a listing that overpromises collects one-star reviews:

- **No comments.** TikTok's comment endpoint was measured to answer either a
  91-byte `status_code: 5` envelope or an **HTTP 200 with zero bytes** unless
  the request carries a rotating `X-Bogus`/`msToken` signature. Reverse-engineering
  a rotating signature is a maintenance treadmill, so this Actor does not
  pretend to offer comments.
- **No video discovery.** You supply the video URLs. Profile pages, tag pages,
  the explore feed and music pages were all measured to carry **no video list**
  server-side — zero ids across every route tried. Pair this with a discovery
  Actor if you need to find videos first.
- **No profile-only scraping.** Pass videos. A profile URL is rejected with a
  message telling you so.
- **No video file downloads or transcripts.**
- **No `vm.tiktok.com` short links.** Expanding one needs a redirect this Actor
  does not follow; it is rejected with a message rather than guessed at.
- **No historical or time-series data.** Every count is as of the moment it was
  scraped. Run it on a schedule if you want a series.
- **Private and deleted videos return an error row, not data.**

### How it works, and the honest part

One plain HTTP GET per video. TikTok server-renders the full video metadata into
the watch page, so there is no browser, no login, no cookie, no API key and — the
part that matters for durability — **no signed request**. Nothing here rotates
on a TikTok deploy.

#### Reliability

- **Measured 19/20 on real videos from Apify datacenter IPs**, the single miss a
  timeout rather than a block. Datacenter is the IP class the platform actually
  runs on, which is why that is the number quoted rather than a residential one.
- **Deleted is distinguishable from blocked.** TikTok answers a missing video
  with HTTP 200 and `statusCode 10204` / `"item doesn't exist"`, where a block
  has no rehydration payload at all. Those become different rows: a typed
  `video-not-found` error that is never billed, versus a retry on a fresh IP.
- **Blocked requests are retried on a different datacenter IP**, twice, with a
  hard backoff. A not-found is never retried — TikTok will say the same thing
  from every IP, so retrying spends requests to learn nothing.
- **The id is checked on the way out.** TikTok serves a video even when the
  handle in the URL is wrong, so the Actor refuses to emit a row whose returned
  id does not match the one requested.
- **The response gate never keys on size.** A not-found page measured 369 KB
  against a real page's 394 KB — close enough that any size threshold would
  misclassify. Only shape is used.

#### robots.txt

You should know this before you buy, so it is here rather than buried.
`https://www.tiktok.com/robots.txt` disallows a broad set of paths and bans
several named crawler user-agents outright. This Actor requests the ordinary
public watch page, sends no credentials, and bypasses no authentication or
access control — it reads what any logged-out visitor sees. Whether that is
acceptable for your purpose is a decision for you and your legal advisers, and
it depends on your jurisdiction and what you do with the data.

#### Proxy

Apify **datacenter** proxy, on by default, and used to rotate IPs when a video
comes back blocked. Residential is deliberately not offered: it is billed per
gigabyte, and at 87 KB per video it would dominate the cost of a run for a
transport that was measured not to need it.

# Actor input Schema

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

Full video URLs (`tiktok.com/@handle/video/123...`), photo-post URLs, `/v/` or `/embed/v2/` links, or bare numeric video ids. Extra query parameters are ignored. A `vm.tiktok.com` short link is rejected with a message rather than guessed at, because expanding it needs a redirect this Actor does not follow.

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

The same videos in the shape other Actors and the Apify UI hand over. Merged with the list above and deduplicated by video id.

## `sourceDatasetId` (type: `string`):

Read video URLs out of another run's dataset, for chaining after a profile or hashtag Actor.

## `sourceDatasetField` (type: `string`):

Which field in that dataset holds the video URL. Defaults to `url`.

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

A ceiling on how many of the supplied videos are scraped. One request per video, so this is also the request count.

## `requestDelayMs` (type: `integer`):

Applied across the whole run, not per worker, so this is the real interval between requests to TikTok whatever the concurrency. Measured from Apify datacenter IPs: 19 of 20 videos hydrated at 4s spacing, the single miss a timeout rather than a block.

## `maxConcurrency` (type: `integer`):

How many videos are fetched at once. The pacing above still bounds the overall request rate.

## `proxyConfig` (type: `object`):

Apify datacenter proxy, on by default, and used to retry a blocked video on a different IP. Residential is deliberately not used: it is billed per gigabyte and the transport was measured not to need it.

## Actor input object example

```json
{
  "videos": [
    "https://www.tiktok.com/@scout2015/video/6718335390845095173"
  ],
  "sourceDatasetId": "aBcDeFgHiJkLmNoPq",
  "sourceDatasetField": "url",
  "maxVideos": 1000,
  "requestDelayMs": 1500,
  "maxConcurrency": 4,
  "proxyConfig": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `dataset` (type: `string`):

One row per video: views, likes, comments, shares, saves, reposts, engagement rate, hashtags, sound, exact post date and the creator's follower count. Videos that could not be scraped get a typed error row instead, and are never charged.

# 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/@scout2015/video/6718335390845095173"
    ]
};

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

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

```

## MCP server setup

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