# TikTok Video Scraper (`apihq/tiktok-video-scraper`) Actor

Export TikTok video metadata to JSON by video URL: caption, views, likes, comments, shares, saves, author, music, duration, cover and caption-track links. Deleted or unavailable videos return success:false, cost $0, and never break the batch. One record per video. $1.75 per 1,000 videos.

- **URL**: https://apify.com/apihq/tiktok-video-scraper.md
- **Developed by:** [apihq dev](https://apify.com/apihq) (community)
- **Categories:** Social media, Videos, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.75 / 1,000 videos

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 Video Scraper

Export the public metadata of TikTok videos as clean JSON from their video URLs: caption, views, likes, comments, shares, saves, author, sound, duration, cover, hashtags, and caption-track links. No TikTok account, no API key, no browser.

The contract is simple: **a deleted or unavailable video never fails your run, and you only pay for videos delivered.** Paste a removed video or a bad URL, and it comes back as a `success: false` row while the rest of the batch keeps going. Pay-per-result at $1.75 per 1,000 videos. Failed videos cost $0.

### See one run

Input:

```json
{
  "postURLs": [
    "https://www.tiktok.com/@scout2015/video/6718335390845095173",
    "https://www.tiktok.com/@apifytech/video/7398101551744552225",
    "https://www.tiktok.com/@looooooooch/video/7332342275151760642",
    "https://www.tiktok.com/@scout2015/video/7000000000000000001"
  ]
}
```

What comes back (counts are from pages captured on 2026-11-02; live numbers move):

- 3 `success: true` records, one per video, all billed. The `apifytech` video, for example: 595 views, 21 likes, 0 comments, 1 share, 1 save, 23 seconds, 576x1024, created in region `CZ`, five hashtags, and one English auto-generated caption track. A video with zero comments is still a full, billed record
- 1 `success: false` row, `errorCode: TIKTOK_VIDEO_UNAVAILABLE`, for the missing video, not billed
- Run status: **SUCCEEDED**
- Charge: 3 delivered videos x $0.00175 = $0.00525

Good input returns billed rows, bad input returns free error rows in the same dataset, and the run still succeeds, so one bad video never costs you a batch.

### Why this Actor

- **A bad video never breaks the batch.** A deleted, private, or unreachable video returns a `success: false` record with a machine-readable `errorCode` (`TIKTOK_VIDEO_UNAVAILABLE`, `VALIDATION_FAILED`, `TIKTOK_TIMEOUT`, ...). Branch on `errorCode` without parsing English.
- **You pay only for videos.** Billing fires on an explicit per-video event, not on dataset writes, so failure rows sit in your dataset for free. There is no per-run start fee, so a run that delivers nothing costs nothing.
- **Drop-in field names.** Records use the field names and nesting most TikTok scrapers already use (`text`, `playCount`, `diggCount`, `commentCount`, `shareCount`, `collectCount`, `createTimeISO`, `webVideoUrl`, `authorMeta`, `musicMeta`, `videoMeta`), so an existing pipeline can switch without a remap.
- **No made-up values.** A field TikTok does not report is `null`, never a guessed `0` or `false`.

### Best for

Data and AI workflows that need the numbers behind specific TikTok videos: campaign and influencer reporting, tracking views and engagement of known videos over time, enriching a list of video links with author and sound data, and building video datasets for analytics or LLM pipelines. The failure-free billing and machine-readable error rows matter most when the extraction runs unattended inside a larger pipeline, where one deleted video should never take down the batch. It works fine for one-off manual pulls too.

### Input example

A single video:

```json
{
  "postURLs": ["https://www.tiktok.com/@scout2015/video/6718335390845095173"]
}
```

A batch of videos, copied from the browser with tracking parameters (they are ignored, and copies of the same video run once):

```json
{
  "postURLs": [
    "https://www.tiktok.com/@scout2015/video/6718335390845095173?is_from_webapp=1&sender_device=pc",
    "https://m.tiktok.com/@scout2015/video/6718335390845095173",
    "https://www.tiktok.com/@apifytech/video/7398101551744552225/"
  ]
}
```

A daily stats refresh of the videos you track (schedule it in the Console and diff `playCount` between runs):

```json
{
  "postURLs": [
    "https://www.tiktok.com/@looooooooch/video/7332342275151760642",
    "https://www.tiktok.com/@apifytech/video/7398101551744552225"
  ]
}
```

| Field | Type | Default | What it does |
|---|---|---|---|
| `postURLs` | array of strings | | TikTok video URLs, up to 50 unique videos per run. Copies of the same video (with `www.`, `m.`, a query string, or a trailing slash) are run and billed once. |

### Output example

One record per video. The author, sound, and media details are nested objects; the Console's table views flatten them to columns such as `authorMeta.name` and `videoMeta.duration`:

```json
{
  "success": true,
  "id": "7398101551744552225",
  "text": "📣 Yes, you can scrape job listings #data in a minute!!   #webscraping #techtiktok #job #automation ",
  "textLanguage": "en",
  "createTime": 1722504752,
  "createTimeISO": "2024-08-01T09:32:32.000Z",
  "isAd": false,
  "isPinned": null,
  "locationCreated": "CZ",
  "privateItem": false,
  "secret": false,
  "forFriend": false,
  "authorMeta": {
    "id": "7353570794285417504",
    "name": "apifytech",
    "nickName": "Apify",
    "profileUrl": "https://www.tiktok.com/@apifytech",
    "avatar": "https://p16-sign-va.tiktokcdn.com/...",
    "signature": "...",
    "verified": false,
    "privateAccount": false,
    "region": null,
    "fans": 3589,
    "following": 1,
    "heart": 4145,
    "video": 63,
    "digg": 66,
    "friends": 0
  },
  "musicMeta": {
    "musicName": "original sound",
    "musicAuthor": "Apify",
    "musicOriginal": true,
    "musicId": "7398101561353718560",
    "musicAlbum": null,
    "playUrl": "https://v16m.tiktokcdn.com/...",
    "coverMediumUrl": "https://p16-sign-va.tiktokcdn.com/..."
  },
  "webVideoUrl": "https://www.tiktok.com/@apifytech/video/7398101551744552225",
  "videoMeta": {
    "duration": 23,
    "height": 1024,
    "width": 576,
    "definition": "540p",
    "format": "mp4",
    "coverUrl": "https://p16-sign-va.tiktokcdn.com/...",
    "originalCoverUrl": "https://p16-sign-va.tiktokcdn.com/...",
    "dynamicCoverUrl": "https://p16-sign-va.tiktokcdn.com/...",
    "subtitleLinks": [
      {
        "language": "eng-US",
        "tiktokLink": "https://v16-webapp.tiktok.com/...",
        "downloadLink": null,
        "source": "ASR",
        "version": "1:big_caption",
        "format": "webvtt",
        "size": 872,
        "expiresAt": "2026-11-04T13:16:40.000Z"
      }
    ]
  },
  "hasSubtitleDescriptors": true,
  "diggCount": 21,
  "shareCount": 1,
  "playCount": 595,
  "commentCount": 0,
  "collectCount": 1,
  "repostCount": 0,
  "hashtags": [
    { "id": "400574", "name": "data", "title": "data", "cover": null },
    { "id": "1622560817599509", "name": "webscraping", "title": "webscraping", "cover": null }
  ],
  "mentions": [],
  "submittedVideoUrl": "https://www.tiktok.com/@apifytech/video/7398101551744552225"
}
```

(Two of the five hashtags shown; signed URLs shortened.)

A video that cannot be read lands in the same dataset and is not charged:

```json
{
  "success": false,
  "url": "https://www.tiktok.com/@scout2015/video/7000000000000000001",
  "input": "https://www.tiktok.com/@scout2015/video/7000000000000000001",
  "errorCode": "TIKTOK_VIDEO_UNAVAILABLE",
  "error": "TikTok reports this video does not exist or is not available",
  "requestId": "req_ea6f91d1d7da4706a7c41189",
  "statusCode": 422
}
```

Split hits from misses on the `success` field. Quote `requestId` in any support issue and we can trace the exact request.

### What you get

Every video is one record with `success: true`. Any value TikTok does not report is `null`:

| Field | Type | What it is |
|---|---|---|
| `success` | boolean | `true` for a video (billed), `false` for a video that could not be read (not billed). |
| `id` | string | The video ID. |
| `text` | string | The caption, possibly empty. |
| `textLanguage` | string or null | Caption language code TikTok reports. |
| `createTime` / `createTimeISO` | integer / string | When the video was posted: Unix seconds and ISO 8601 UTC. |
| `playCount`, `diggCount`, `commentCount`, `shareCount` | integer | Views, likes, comments, shares. Always present. |
| `collectCount`, `repostCount` | integer or null | Saves and reposts. |
| `webVideoUrl` | string | The canonical video URL, with the author's current handle. |
| `submittedVideoUrl` | string | The input URL this record answers, as you gave it. |
| `authorMeta` | object | `id`, `name` (handle), `nickName`, `profileUrl`, `avatar`, `signature` (bio), `verified`, `privateAccount`, `region`, `fans`, `following`, `heart` (likes received), `video` (videos posted), `digg` (videos liked), `friends`. |
| `musicMeta` | object | `musicName`, `musicAuthor`, `musicOriginal`, `musicId`, `musicAlbum`, `playUrl`, `coverMediumUrl`. All `null` when TikTok reports no sound. |
| `videoMeta` | object | `duration` (seconds), `width`, `height`, `definition`, `format`, `coverUrl`, `originalCoverUrl`, `dynamicCoverUrl`, `subtitleLinks`. |
| `videoMeta.subtitleLinks` | array or null | Caption tracks TikTok advertises: `language`, `tiktokLink`, `downloadLink` (always `null`), `source` (`ASR` = auto-generated), `version`, `format`, `size` (bytes), `expiresAt`. `[]` when TikTok lists none. |
| `hasSubtitleDescriptors` | boolean or null | `true` when at least one caption track is listed, `false` when none, `null` when TikTok did not say. |
| `hashtags` | array or null | Hashtags in the caption: `id`, `name`, `title`, `cover`. |
| `mentions` | array or null | `@handles` mentioned in the caption. |
| `isAd`, `privateItem`, `secret`, `forFriend` | boolean or null | TikTok's own flags for the video. |
| `isPinned` | null | Always `null`: the video page does not say whether a video is pinned on its profile. |
| `locationCreated` | string or null | Region code TikTok reports for where the video was created. Not the author's location. |

IDs are strings because TikTok's 19-digit IDs do not fit in a JavaScript number. Media and caption URLs are TikTok's signed links: they expire (see `expiresAt`) and may only work from some networks.

Videos that could not be read carry `success: false`, `url` and `input` (the URL you gave), a machine-readable `errorCode`, a human-readable `error`, the service `requestId`, and the HTTP `statusCode`. They are not charged:

| Code | Meaning |
|---|---|
| `TIKTOK_VIDEO_UNAVAILABLE` | The video is deleted, private, or otherwise unavailable. |
| `TIKTOK_UNAVAILABLE` | TikTok did not serve this video and gave no reason. |
| `TIKTOK_BLOCKED` | TikTok refused the request after an automatic retry. |
| `TIKTOK_RATE_LIMITED` | TikTok rate-limited the request after an automatic retry. |
| `TIKTOK_TIMEOUT` | TikTok did not answer in time after an automatic retry. |
| `DEADLINE_EXCEEDED` | The video request hit its 30-second deadline. |
| `VALIDATION_FAILED` | The URL is not a supported TikTok video URL (including short links and photo posts), or TikTok rejected the video ID. |
| `NO_INPUT` | No usable `postURLs` were given. The run ends successfully with this single row. |

### Pricing

Pay-per-result. One charge per video delivered: `$0.00175` each, which is `$1.75 per 1,000 videos`. The charge fires only after a video record lands in your dataset.

- 1,000 delivered videos cost $1.75.
- 10,000 delivered videos cost $17.50.
- A run that hits only deleted or unavailable videos costs $0.
- There is no per-run start fee, so an empty run is genuinely free.

Platform compute is included in the per-video price. Cap the maximum spend of a single run from Apify's Run Limits panel; the Actor checks the cap before each video and stops cleanly when the budget runs out.

### What this Actor does not do

Honest scope, so you know before you run it:

- **Video URLs only.** It reads the videos you give it. It does not find videos from a profile, hashtag, sound, or search query, and it does not fetch related videos.
- **Full video URLs only.** Short share links (`vm.tiktok.com/...`, `vt.tiktok.com/...`) and photo/slideshow posts (`/photo/...`) come back as a free `success: false` row with `errorCode: VALIDATION_FAILED`. Open a short link in a browser and paste the full `https://www.tiktok.com/@user/video/<id>` URL instead.
- **No downloads.** It does not download videos, covers, audio, or caption files. Media and caption URLs are returned as TikTok serves them.
- **No transcripts.** `subtitleLinks` lists the caption tracks TikTok advertises; the Actor does not fetch or parse their text.
- **Only what the video page says.** Pinned status is not on the page (`isPinned` is always `null`), and author `region` is usually `null`. Ad, sponsorship, and shop flags beyond `isAd` are not reported.
- **No login-only content.** Private videos and anything that needs a TikTok account return `success: false`.

### How to use this Actor

1. Open the Actor in the Apify Console.
2. Paste one or more TikTok video URLs into `postURLs`. Up to 50 unique videos run per job.
3. Click Start. Each video is one `success: true` record. Videos that cannot be read produce one `success: false` record and do not stop the run. Only delivered videos are charged.

The Actor is also callable from the Apify API and every official integration (Make, Zapier, n8n, Slack, webhooks). The API tab in the Console has ready-to-paste JavaScript, Python, and curl snippets.

### Reliability

**A bad video never fails the batch.** A deleted video, a bad URL, or a video TikTok will not serve becomes a `success: false` record with a specific `errorCode`. The run keeps going and finishes successfully.

**You never pay for a failure.** Billing fires on an explicit per-video charge event, not on dataset writes, so `success: false` records are free. There is no per-run start fee either.

**Automatic retry, then a clear answer.** A blocked, rate-limited, or slow request is retried once from a fresh network identity before the video is reported as failed. Each video runs under a 30-second deadline, so a stalled page returns a `success: false` record with `errorCode: DEADLINE_EXCEEDED` instead of hanging your run.

**No duplicate charges.** The same video pasted several times in one input (with `m.`, a query string, or a trailing slash) is fetched and charged once per run.

**Genuine faults are not hidden.** An outage of our backend, a page TikTok changed in a way we cannot read, or a failure to write or bill results fails the run, so a broken run never looks like a clean one with missing data.

### FAQ

**Do I need a TikTok account or API key?**

No. The Actor reads public video pages directly. There is nothing to log into and no TikTok developer key to provision. (You run it with your normal Apify account, like any Actor.)

**What URL formats are accepted?**

Full TikTok video URLs: `https://www.tiktok.com/@user/video/<id>`, also with `m.tiktok.com`, without `www.`, with a query string, or with a trailing slash. Short links (`vm.tiktok.com`, `vt.tiktok.com`) and photo posts are not supported and return a free `success: false` row with `errorCode: VALIDATION_FAILED`.

**Is the output compatible with other TikTok video scrapers?**

Yes for the core fields: `text`, `createTimeISO`, `playCount`, `diggCount`, `commentCount`, `shareCount`, `collectCount`, `webVideoUrl`, and the nested `authorMeta`, `musicMeta`, and `videoMeta` objects use the names most TikTok scrapers already use. Failure records name the error in `errorCode`, but the code values are this Actor's own (listed above).

**Why is a field `null`?**

TikTok did not report it for that video. The Actor never fills a gap with a guessed `0` or `false`, so `null` always means "unknown", not "none".

**Can I get the transcript of a video?**

Not with this Actor. It tells you whether caption tracks exist (`hasSubtitleDescriptors`) and lists their links in `videoMeta.subtitleLinks`, but it does not fetch the caption text.

**Can I download the video file?**

No. The Actor returns metadata and TikTok's own media links only.

**How much does it cost?**

$1.75 per 1,000 delivered videos. 10,000 videos cost $17.50. Failed videos and empty runs cost $0, and there is no start fee.

**What if a video was deleted or is private?**

It comes back as a `success: false` record with `errorCode: TIKTOK_VIDEO_UNAVAILABLE`. You are not charged, and the other videos in the batch keep running.

**Can I also get the comments of these videos?**

Yes, with the sibling Actor [TikTok Comments Scraper](https://apify.com/apihq/tiktok-comments-scraper), which takes the same `postURLs` and uses the same failure-free billing.

**Can I use this Actor from my own code?**

Yes. Use the Apify API or one of the official SDKs (Node.js: `apify-client`, Python: `apify-client`). The Console shows ready-to-paste code samples on the API tab.

### Found a bug or want a feature?

Open an issue on this Actor's Issues tab and include the `requestId` from any error record you saw. We respond within one business day.

# Actor input Schema

## `postURLs` (type: `array`):

TikTok video URLs in the form https://www.tiktok.com/@user/video/<id>. Aliases of the same video are de-duplicated; up to 50 unique videos are processed per run. Short vm.tiktok.com / vt.tiktok.com links and /photo/ posts are not supported and come back as a success:false record with errorCode VALIDATION_FAILED. A video that is unavailable or cannot be read comes back as a success:false record, so one bad video never blocks the batch.

## Actor input object example

```json
{
  "postURLs": [
    "https://www.tiktok.com/@scout2015/video/6718335390845095173"
  ]
}
```

# Actor output Schema

## `videos` (type: `string`):

One record per input video. success:true carries the video metadata with nested authorMeta, musicMeta and videoMeta; success:false carries errorCode and error for a video that failed.

# 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 = {
    "postURLs": [
        "https://www.tiktok.com/@scout2015/video/6718335390845095173"
    ]
};

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

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

```

## MCP server setup

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