# TikTok Profile Scraper (`apihq/tiktok-profile-scraper`) Actor

Export every accessible video of public TikTok profiles to JSON: caption, views, likes, comments, shares, saves, music, duration, cover, pin flag and the creator's profile stats. Date filters, per-profile caps or the full history. Unavailable and private profiles return success:false and cost $0.

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

## Pricing

$0.90 / 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 Profile Scraper

Export the public videos of TikTok profiles as clean JSON from their handles or profile URLs: caption, views, likes, comments, shares, saves, posting date, sound, duration, cover, hashtags, and the creator's followers, following, and total likes on every row. Walk a creator's full accessible video history or cap it per profile, and filter by posting date. No TikTok account, no API key, no browser.

The contract is simple: **a private or unavailable profile never fails your run, and you only pay for videos delivered.** Paste a private account, a missing creator, or a bad URL, and it comes back as a `success: false` row while the other profiles keep going. $0.90 per 1,000 videos. Failed profiles and profile rows cost $0, and there is no start fee.

### See one run

Input:

```json
{
  "profiles": ["apifytech", "m9_no_such_creator_20261103"],
  "resultsPerPage": 0
}
```

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

- 57 `itemType: "video"` records for `apifytech`, newest first, all billed. That is every video TikTok lists for the creator, from 2026-10-29 back to 2024-04-03, walked to TikTok's natural end of the list. The profile itself reports 63 videos; TikTok serves 57 (see [below](#what-this-actor-does-not-do)). The newest one: 780 views, 7 likes, 0 comments, 1 share, 2 saves, 55.68 seconds, five hashtags. Every row carries the creator's profile snapshot: 3,591 followers, 1 following, 4,145 likes
- 1 `itemType: "error"` row, `errorCode: TIKTOK_PROFILE_UNAVAILABLE`, for the handle TikTok does not serve, not billed
- Run status: **SUCCEEDED**
- Charge: 57 delivered videos x $0.0009 = $0.0513

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

### Why this Actor

- **The full list when you ask for it.** `resultsPerPage: 0` walks each profile until TikTok's own list ends. Every profile tells you why its walk stopped (`stopReason`) and whether it reached the end (`complete`), so a capped or interrupted walk never looks like a full history.
- **You pay only for videos.** Billing fires on an explicit per-video event, not on dataset writes. Profile rows, error rows, and the run summary are free, date filters cost nothing extra, and there is no per-run start fee or minimum.
- **No video billed twice.** Aliases of the same handle (`apifytech`, `@ApifyTech`, the profile URL) run once, two inputs that turn out to be the same creator are walked once, and a video is never charged twice in a run.
- **Drop-in field names.** Inputs (`profiles`, `resultsPerPage`, `oldestPostDateUnified`, `newestPostDate`) and video records (`text`, `playCount`, `diggCount`, `commentCount`, `shareCount`, `collectCount`, `createTimeISO`, `webVideoUrl`, `isPinned`, `authorMeta`, `musicMeta`, `videoMeta`, `input`) use the names most TikTok profile scrapers already use, 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 what creators post and how it performs: influencer vetting and creator discovery, competitor and brand-account monitoring, campaign reporting over a date window, tracking a roster of creators on a schedule, and building creator video datasets for analytics or LLM pipelines. The free error rows and explicit stop reasons matter most when the walk runs unattended inside a larger pipeline, where one private or deleted account should never take down the batch. It works fine for one-off manual pulls too.

### Input example

One profile, its full accessible video history:

```json
{
  "profiles": ["apifytech"],
  "resultsPerPage": 0
}
```

Several profiles, the 40 newest videos of each, given as a handle, an `@handle`, and a profile URL copied from the browser (the query string is ignored, and the URL is the same creator as the first entry, so it runs once):

```json
{
  "profiles": [
    "apifytech",
    "@scout2015",
    "https://www.tiktok.com/@apifytech?lang=en"
  ],
  "resultsPerPage": 40
}
```

Every video posted in the first half of 2026. Both dates are inclusive and in UTC: `2026-06-30` covers that whole day. The walk stops at the first page whose non-pinned videos are all older than `2026-01-01`, so the rest of the history is not walked:

```json
{
  "profiles": ["apifytech"],
  "resultsPerPage": 0,
  "oldestPostDateUnified": "2026-01-01",
  "newestPostDate": "2026-06-30"
}
```

The 10 newest videos of each creator plus one free profile row per creator with its stats and how the walk ended:

```json
{
  "profiles": ["apifytech", "scout2015"],
  "resultsPerPage": 10,
  "includeProfileRow": true
}
```

| Field | Type | Default | What it does |
|---|---|---|---|
| `profiles` | array of strings | | Public TikTok handles (`apifytech`), `@handles`, or profile URLs (`https://www.tiktok.com/@apifytech`, also with `m.` or without `www.`; query strings and `#` fragments are ignored). Up to 50 unique profiles per run; extra ones are dropped with a warning in the log. Aliases of the same handle (case-insensitive) run once. |
| `resultsPerPage` | integer | `100` | Maximum videos delivered **per profile**, counting only videos that match your date window. `0` means all accessible videos: the walk runs to TikTok's natural end of the list, or until the run's cost limit. |
| `oldestPostDateUnified` | string | blank | Only videos posted at or after this moment. `YYYY-MM-DD` (starts at 00:00 UTC) or an ISO timestamp with a zone (`2026-01-01T12:00:00Z`, `2026-01-01T12:00:00+02:00`). Relative values such as "7 days" are not accepted. |
| `newestPostDate` | string | blank | Only videos posted at or before this moment. Same formats; a date-only value includes that whole UTC day. |
| `includeProfileRow` | boolean | `false` | Adds one free `itemType: "profile"` row per creator. A creator with zero delivered videos always gets one, whatever this is set to. |

An invalid date, a timestamp without a zone, or `oldestPostDateUnified` later than `newestPostDate` stops the run before any request to TikTok: you get one free `success: false` row with `errorCode: VALIDATION_FAILED` and `statusCode: 400` naming the field, no charge, and the run still succeeds.

### Output example

Every record has `itemType`: `video` (billed), `profile` (free), or `error` (free). The author, sound, and media details are nested objects; the Console's table views flatten them to columns such as `authorMeta.fans` and `videoMeta.duration`. One video:

```json
{
  "success": true,
  "itemType": "video",
  "id": "7702131657784216854",
  "text": "nobody wants to copy Instagram reel stats by hand, so we built an Actor for that 👀 #aiagents #automation #aitools #ai #apify",
  "textLanguage": null,
  "createTime": 1793292277,
  "createTimeISO": "2026-10-29T16:44:37.000Z",
  "isAd": false,
  "isPinned": false,
  "locationCreated": null,
  "privateItem": null,
  "secret": null,
  "forFriend": null,
  "authorMeta": {
    "id": "7353570794285417504",
    "name": "apifytech",
    "nickName": "Apify",
    "profileUrl": "https://www.tiktok.com/@apifytech",
    "avatar": "https://p16-sign-va.tiktokcdn.com/...",
    "signature": "...",
    "bioLink": "apify.it/m/tiktok",
    "verified": false,
    "privateAccount": false,
    "region": null,
    "commerceUserInfo": { "commerceUser": false },
    "ttSeller": false,
    "roomId": "",
    "fans": 3591,
    "following": 1,
    "heart": 4145,
    "video": 63,
    "digg": 0,
    "friends": 1
  },
  "musicMeta": {
    "musicName": "original sound",
    "musicAuthor": "Apify",
    "musicOriginal": false,
    "musicId": "7702131695763409686",
    "musicAlbum": "",
    "playUrl": "https://sf16-ies-music-va.tiktokcdn.com/...",
    "coverMediumUrl": "https://p16-sign-va.tiktokcdn.com/..."
  },
  "webVideoUrl": "https://www.tiktok.com/@apifytech/video/7702131657784216854",
  "videoMeta": {
    "duration": 55.68,
    "height": 3840,
    "width": 2160,
    "definition": "540p",
    "format": null,
    "coverUrl": "https://p16-sign-va.tiktokcdn.com/...",
    "originalCoverUrl": "https://p16-sign-va.tiktokcdn.com/...",
    "dynamicCoverUrl": "https://p16-sign-va.tiktokcdn.com/...",
    "subtitleLinks": null
  },
  "hasSubtitleDescriptors": null,
  "diggCount": 7,
  "shareCount": 1,
  "playCount": 780,
  "commentCount": 0,
  "collectCount": 2,
  "repostCount": 0,
  "hashtags": [
    { "id": "7175202785762213930", "name": "aiagents", "title": "aiagents", "cover": null },
    { "id": "8617580", "name": "automation", "title": null, "cover": null }
  ],
  "mentions": [],
  "input": "apifytech"
}
```

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

A profile row, written for every creator when `includeProfileRow` is `true`, and always for a creator with zero delivered videos. It has no video fields and is not charged:

```json
{
  "success": true,
  "itemType": "profile",
  "input": "apifytech",
  "profileUrl": "https://www.tiktok.com/@apifytech",
  "authorMeta": {
    "id": "7353570794285417504",
    "name": "apifytech",
    "nickName": "Apify",
    "fans": 3591,
    "following": 1,
    "heart": 4145,
    "video": 63,
    "digg": 0,
    "friends": 1
  },
  "videosDelivered": 57,
  "stopReason": "source_exhausted",
  "complete": true
}
```

(`authorMeta` shortened; it is the same object as on the video rows.)

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

```json
{
  "success": false,
  "itemType": "error",
  "url": "m9_no_such_creator_20261103",
  "input": "m9_no_such_creator_20261103",
  "errorCode": "TIKTOK_PROFILE_UNAVAILABLE",
  "error": "TikTok did not make this profile available (it may not exist, be deleted or be banned)",
  "requestId": "req_7f3a9c1e5b2d4086a1c4e9f2",
  "statusCode": 422,
  "videosDelivered": 0
}
```

Split videos from the rest on `itemType` (or on `success` for hits versus misses). Quote `requestId` in any support issue and we can trace the exact request.

### What you get

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

| Field | Type | What it is |
|---|---|---|
| `success` | boolean | `true` for video and profile rows, `false` for error rows. |
| `itemType` | string | `video` (billed), `profile` (free), or `error` (free). |
| `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. |
| `isAd` | boolean or null | TikTok's ad flag for the video. |
| `isPinned` | boolean or null | `true` when TikTok marks the video as pinned on the profile, `false` when it marks it as not pinned, `null` when TikTok did not say. |
| `webVideoUrl` | string | The canonical video URL. |
| `input` | string | The profile this record answers, as you gave it. |
| `authorMeta` | object | The creator, from their profile page at the time of the run and identical on every row of that creator: `id`, `name` (handle), `nickName`, `profileUrl`, `avatar`, `signature` (bio), `bioLink` (as TikTok shows it, possibly without `https://`), `verified`, `privateAccount`, `region`, `commerceUserInfo` (`commerceUser`), `ttSeller`, `roomId`, `fans` (followers), `following`, `heart` (likes received), `video` (videos the profile **reports**, not the number delivered), `digg` (videos liked), `friends`. |
| `musicMeta` | object | `musicName`, `musicAuthor`, `musicOriginal`, `musicId` (string), `musicAlbum`, `playUrl`, `coverMediumUrl`. |
| `videoMeta` | object | `duration` (seconds, with fractions such as `55.68`), `height`, `width`, `definition` (such as `540p`), `coverUrl`, `originalCoverUrl`, `dynamicCoverUrl`. `format` and `subtitleLinks` are always `null` here. |
| `hashtags` | array or null | Hashtags from TikTok's structured caption data, not parsed from the caption text: `id`, `name`, `title`, `cover`. A caption with `#` words can still have none here. `null` when TikTok sent no structured data. |
| `mentions` | array or null | `@handles` mentioned in the caption, `[]` when the caption structure has none, `null` when unknown. |
| `locationCreated`, `privateItem`, `secret`, `forFriend`, `hasSubtitleDescriptors` | null | Always `null` here: the profile video list does not report them. |

IDs are strings because TikTok's 19-digit IDs do not fit in a JavaScript number. Media, cover, and music URLs are TikTok's signed links: they expire and may only work from some networks.

Profile rows (`itemType: "profile"`, free) carry `input`, `profileUrl`, `authorMeta` (as above), `videosDelivered`, `stopReason`, and `complete`. `complete` is `true` only when the walk reached TikTok's natural end of the creator's list. `stopReason` says why the walk of a profile ended:

| `stopReason` | Meaning |
|---|---|
| `source_exhausted` | TikTok's list ended. The only reason with `complete: true`. |
| `result_limit` | `resultsPerPage` videos were delivered. |
| `date_boundary` | The walk passed `oldestPostDateUnified`; the rest of the list is older. |
| `budget` | The run's cost limit was reached. |
| `cursor_stalled` | TikTok's pagination stopped moving forward. A free `TIKTOK_UNAVAILABLE` error row says so; videos already delivered are kept. |
| `idle_pages` | Three pages in a row brought no new videos. A free `TIKTOK_UNAVAILABLE` error row says so; videos already delivered are kept. |
| `expected_error` | A page failed with one of the error codes below. Videos already delivered are kept. |

Profiles that could not be read, or whose walk failed or stalled partway, carry `success: false`, `itemType: "error"`, `url` and `input` (the profile you gave), a machine-readable `errorCode`, a human-readable `error`, the service `requestId` and HTTP `statusCode` when a request failed, and `videosDelivered` (videos already delivered for that profile before the failure). They are not charged:

| Code | Meaning |
|---|---|
| `TIKTOK_PROFILE_UNAVAILABLE` | TikTok does not serve this profile. It may not exist, be deleted, or be banned; TikTok does not say which. |
| `TIKTOK_PRIVATE_ACCOUNT` | The account is private. Its videos are never requested. |
| `TIKTOK_UNAVAILABLE` | TikTok did not serve the profile or a page of videos and gave no reason, or its list stopped yielding before its end (`stopReason` `cursor_stalled` or `idle_pages`). |
| `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 request hit its 30-second deadline. |
| `VALIDATION_FAILED` | The input is not a supported profile (short links `vm.tiktok.com`/`vt.tiktok.com`, video URLs, other sites, URLs with credentials or a port), or a date filter is invalid (then it is the only row and no profile is requested). |
| `NO_INPUT` | No usable `profiles` were given. The run ends successfully with this single row. |

The run also writes a summary to the `OUTPUT` record of its default key-value store, one entry per profile, including `input`, `status`, `profileId`, `uniqueId`, `videosDelivered`, `pagesFetched`, `stopReason`, and `complete`. It is free, and it is there even when `includeProfileRow` is `false`.

### Pricing

Pay per event. One `video` charge per unique video delivered: `$0.0009` each, which is `$0.90 per 1,000 videos`. The charge fires only after the video record lands in your dataset.

- 1,000 delivered videos cost $0.90, whether they come from one creator or from fifty.
- A full walk of `apifytech` (57 accessible videos) is 57 charges.
- Profile rows, error rows, and the `OUTPUT` summary cost $0. So do private, unavailable, and invalid profiles.
- Date filters cost nothing extra: videos outside the window are skipped and not charged.
- There is no per-run start fee and no minimum, so a run that delivers nothing costs nothing.

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 profile and each page of videos, and stops cleanly with `stopReason: budget` when it runs out.

### What this Actor does not do

Honest scope, so you know before you run it:

- **Public accounts only.** A private account returns a free `success: false` row with `errorCode: TIKTOK_PRIVATE_ACCOUNT`, and its videos are never requested. Nothing that needs a TikTok login is returned.
- **Accessible videos, not the profile's count.** The number of videos TikTok serves can be lower than the count the profile shows. `apifytech` reports 63 videos, and a full walk returns 57. `authorMeta.video` is the count the profile reports; `complete: true` means the walk reached the end of TikTok's list, not that it matched that count.
- **Newest first only.** Videos come in TikTok's order, newest first, with pinned videos possibly at the top. There is no oldest-first or most-popular sorting.
- **Pins as TikTok reports them.** `isPinned` is filled when TikTok sends it, but pinned videos are not guaranteed to come first or to be present.
- **Videos only.** No stories, reposts, liked videos, follower or following lists, comments, transcripts, or video downloads. For comments use the [TikTok Comments Scraper](https://apify.com/apihq/tiktok-comments-scraper), for captions as text the [TikTok Transcript Scraper](https://apify.com/apihq/tiktok-transcript-scraper), and for the full detail of specific videos, including caption-track links, the [TikTok Video Scraper](https://apify.com/apihq/tiktok-video-scraper).
- **Profiles only.** It does not search TikTok or list videos by hashtag or sound. Short links (`vm.tiktok.com/...`, `vt.tiktok.com/...`) and video URLs come back as a free `VALIDATION_FAILED` row. Open a short link in a browser and paste the creator's `https://www.tiktok.com/@handle` URL instead.
- **ISO dates only.** `oldestPostDateUnified` and `newestPostDate` take a date or a timestamp with a zone. Relative values such as "7 days" or "1 month" are rejected.
- **A snapshot, not a feed.** Counts are what TikTok showed at the time of the run, and media, cover, and music URLs are TikTok's signed links that expire. No files are downloaded.
- **No ban diagnosis.** For a profile TikTok does not serve, TikTok does not say whether it never existed, was deleted, or was banned, so neither does the Actor.

### How to use this Actor

1. Open the Actor in the Apify Console.
2. Paste one or more TikTok handles or profile URLs into `profiles`. Up to 50 unique profiles run per job.
3. Set `resultsPerPage` to cap videos per profile, or `0` for all accessible videos. Optionally set `oldestPostDateUnified` and `newestPostDate` for a date window, and `includeProfileRow` for a free stats row per creator.
4. Click Start. Each video is one `success: true` record. Profiles that cannot be read produce one free `success: false` record and do not stop the run. Only 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 profile never fails the batch.** A private account, a missing creator, or a bad URL 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 profile rows, error rows, and the summary are free. There is no per-run start fee either.

**Partial walks say so.** If a page fails partway through a creator, or TikTok's list stalls or keeps repeating itself, the videos already delivered stay in your dataset, a free error row records how many (`videosDelivered`), and the profile's `stopReason` says why (`expected_error`, `cursor_stalled` or `idle_pages`). A walk that stops for a cap, a date, the budget, or a stalled list is never reported as `complete`.

**Pinned posts do not cut a date walk short.** With `oldestPostDateUnified` set, the walk stops early only when every non-pinned video on a page is older than your date, so an old pinned video at the top of a profile never ends the walk.

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

**No duplicate charges.** Aliases of one handle run once, a creator reached through two different inputs is walked once, and a video is charged at most once per run.

**Genuine faults are not hidden.** An outage of our backend, a response 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

**How do I scrape all videos from a TikTok profile?**

Set `resultsPerPage` to `0`. The Actor walks the creator's videos newest first until TikTok's list ends, or until the run's cost limit. The profile row and the `OUTPUT` summary show `stopReason: source_exhausted` and `complete: true` when it got to the end.

**Why do I get fewer videos than the profile shows?**

TikTok often serves fewer videos than the count on the profile: `apifytech` shows 63 and serves 57 to a full walk. The Actor returns every video TikTok lists and does not invent the rest. Check `complete`: `true` means the walk reached the end of TikTok's list. If it is `false`, `stopReason` tells you what stopped it, such as your `resultsPerPage` cap or the run's cost limit.

**Can I scrape private TikTok accounts?**

No. A private account returns a free `success: false` row with `errorCode: TIKTOK_PRIVATE_ACCOUNT`, and its videos are never requested. The Actor does not log in.

**How do I get only videos from a date range?**

Set `oldestPostDateUnified` and/or `newestPostDate` to a date (`2026-01-01`) or a timestamp with a zone (`2026-01-01T12:00:00Z`). Both ends are inclusive, and a date-only `newestPostDate` covers that whole UTC day. Videos outside the window are skipped and not charged, and the walk stops once it passes the oldest date. Combine with `resultsPerPage: 0` to get every video in the window.

**Do I pay for failed profiles?**

No. Private, unavailable, and invalid profiles come back as free `success: false` rows, and the other profiles keep running. If a walk fails partway, you pay only for the videos already delivered.

**Is it compatible with Clockworks TikTok Profile Scraper output?**

For the core contract, yes. The inputs `profiles`, `resultsPerPage`, `oldestPostDateUnified`, and `newestPostDate` have the same names, and video records use the familiar `text`, `createTimeISO`, `playCount`, `diggCount`, `commentCount`, `shareCount`, `collectCount`, `webVideoUrl`, `isPinned`, `input`, and nested `authorMeta`, `musicMeta`, and `videoMeta`. Differences: every record has an `itemType`, `resultsPerPage: 0` means all accessible videos, dates are ISO only (no relative values), there are no sorting or repost/story options, and failure records use this Actor's own `errorCode` values (listed above).

**How do I get profile stats without videos?**

Rows are videos, and every video row carries the creator's stats in `authorMeta`. Set `includeProfileRow: true` to also get one free profile row per creator, and a small `resultsPerPage` (such as `1`) so you pay for few videos. A creator with zero videos to deliver always gets a free profile row with its stats.

**Are pinned videos included?**

Yes, as TikTok lists them, usually at the top. `isPinned` is `true` or `false` when TikTok says, `null` when it does not. Pinned videos are not guaranteed to come first or to be present.

**What profile formats are accepted?**

Handles (`apifytech`), `@handles`, and profile URLs: `https://www.tiktok.com/@apifytech`, also with `m.tiktok.com`, without `www.`, with a query string, or with a `#` fragment. Short links, video URLs, and URLs with credentials or a port return a free `VALIDATION_FAILED` row.

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

No. The Actor reads public profiles 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.)

**How much does it cost?**

$0.90 per 1,000 delivered videos, one charge per unique video. Profile rows, failed profiles, and empty runs cost $0, date filters are included, and there is no start fee.

**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 also get comments or transcripts of these videos?**

Yes, with the sibling Actors [TikTok Comments Scraper](https://apify.com/apihq/tiktok-comments-scraper) and [TikTok Transcript Scraper](https://apify.com/apihq/tiktok-transcript-scraper). Pass them the `webVideoUrl` values from this Actor's output as `postURLs`. They use 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

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

Handles (apifytech), @handles or profile URLs (https://www.tiktok.com/@apifytech). Aliases of the same handle are processed once; up to 50 unique profiles per run. Short vm.tiktok.com / vt.tiktok.com links and video URLs are not supported and come back as a free success:false record with errorCode VALIDATION_FAILED. Private, unavailable or blocked profiles come back as free success:false records, so one bad profile never blocks the batch.

## `resultsPerPage` (type: `integer`):

Maximum delivered videos per profile (matching the date filters, never billed twice). 0 = every accessible video: the walk runs to the natural end of the profile's list or until the run's cost limit. Not an HTTP page size.

## `oldestPostDateUnified` (type: `string`):

Only videos posted on or after this moment. An ISO date (2026-01-31, from 00:00 UTC) or an ISO timestamp with a zone (2026-01-31T12:00:00Z or +02:00). Relative values such as '7 days' are not supported. Once a page's non-pinned videos are all older than this, the profile's walk stops.

## `newestPostDate` (type: `string`):

Only videos posted on or before this moment. An ISO date (2026-01-31 includes that whole UTC day) or an ISO timestamp with a zone. Newer videos are skipped, not billed.

## `includeProfileRow` (type: `boolean`):

Adds one free record with the creator's profile stats, videos delivered and why the walk stopped. A profile with no delivered videos always gets this record.

## Actor input object example

```json
{
  "profiles": [
    "apifytech"
  ],
  "resultsPerPage": 20,
  "includeProfileRow": false
}
```

# Actor output Schema

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

All records in the default dataset; filter on itemType. Videos carry nested authorMeta, musicMeta and videoMeta.

## `summary` (type: `string`):

Per profile: status, videos delivered, pages fetched, stop reason and whether the list was walked to its natural end.

# 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": [
        "apifytech"
    ],
    "resultsPerPage": 20
};

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

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

```

## MCP server setup

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