# TikTok Shop Scraper - $1.10 per 1,000 Products (`dami_studio/tiktok-shop-scraper`) Actor

Units sold sits on the card of every TikTok Shop product, and it is what TikTok's own best-seller pages rank on. Search terms, collection URLs and store URLs mix in one run. Title, price, original price, discount, rating, reviews, shop, link, images. US storefront. $1.10/1,000.

- **URL**: https://apify.com/dami\_studio/tiktok-shop-scraper.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** E-commerce, Social media, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.10 / 1,000 product scrapeds

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?

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

Units sold sits on the card of every TikTok Shop product, and it is the number TikTok's own best-seller pages rank on. Search terms, TikTok Shop collection URLs and store URLs all go into the same run. Products come back as a table: title, price and currency, the crossed-out original price and the discount, star rating, review count, units sold, which shop sells it, the product link and the images.

No account, no API key, no browser.

- Three ways in: a plain search term, a collection page URL, or a store page URL. Mix them in one run.
- Units sold and star rating land on the same row as the price, so sorting a category by `soldCount` doesn't need a second lookup.
- Sort a search by best sellers or by newest, not just the site default.
- Rows are deduplicated on product id across the whole run, so a product that matches two of your terms is charged once.
- Empty input returns one labelled sample row, free.

### Price

**$1.10 per 1,000 products.** Plus **$0.001 per run** as the platform start fee, billed per gigabyte of run memory, so exactly that on the default 1 GB.

No volume tiers, no minimum spend, no subscription.

| Products | Total |
|---|---|
| 100 | $0.111 |
| 1,000 | $1.101 |
| 10,000 | $11.001 |
| 100,000 | $110.001 |

#### What gets charged

- One `product-scraped` event per product row written to the dataset. Nothing else is metered per row.
- Free: the sample row an empty run returns, and every diagnostic row. They all carry `"charged": false`.
- Duplicate products that already appeared earlier in the same run are dropped before they're charged.
- Products the page rendered but that carry no title are skipped rather than delivered as an empty row.
- A run that finds nothing costs the start fee and nothing else.
- Rows never leave the dataset without a charge and are never charged without a row. The billed event is a named one, so nothing is quietly attached to `apify-default-dataset-item`.

### Input

```json
{
  "searchTerms": [
    "makeup brush",
    "wireless earbuds"
  ],
  "startUrls": [
    "https://shop.tiktok.com/us/k/probiotic-sodas",
    "https://www.tiktok.com/shop/store/bk-beauty-brushes/7495182121564604692"
  ],
  "sortBy": "best_sellers",
  "maxItems": 200
}
```

| Field | What it does |
|---|---|
| `searchTerms` | Keywords to search TikTok Shop for. Each term is searched in turn until the run reaches `maxItems`. |
| `startUrls` | TikTok Shop pages to read instead of, or as well as, search terms. Collection pages (`/shop/k/...`), store pages (`/shop/store/<name>/<seller id>`) and search pages (`/shop/s/...`) are all understood. A bare seller id works too. Anything else gets a free diagnostic row telling you why it was skipped. |
| `sortBy` | How products come back for a search term or a collection URL: `relevance` (site default), `best_sellers` (most units sold first) or `newest`. Store pages always come back in the shop's own order. |
| `maxItems` | Total rows across every term and URL. The budget is split evenly between targets, so four targets and 100 rows gives you 25 of each. Default 50, hard ceiling 5,000. Keep it low while testing, since you pay per product. |
| `proxyUrls` | Leave empty. Only for callers who want traffic to leave through proxy servers they already pay for, as `http://user:pass@host:port`. They have to exit in a country where TikTok Shop operates. |
| `sessionCookies` | Leave empty. Runs work with nothing filled in. See [Shop pages behind a bot check](#shop-pages-behind-a-bot-check). |

Run it with empty input and you get one labelled sample row, free.

### Output

One row per product. This is a real row from a real run:

```json
{
  "ok": true,
  "charged": true,
  "recordType": "product",
  "productId": "1732304214281785959",
  "title": "A set of 13 professional makeup brushes, including soft fur eyeshadow brushes, foundation brushes, and blush brushes, for creating a perfect makeup look",
  "price": 1.72,
  "currency": "USD",
  "priceFormatted": "$1.72",
  "originalPrice": 4.19,
  "discountPercent": 59,
  "rating": 5,
  "reviewCount": 1,
  "soldCount": 24,
  "shopName": "Suitable beauty tools",
  "sellerId": "8654790995160371815",
  "shopUrl": "https://www.tiktok.com/shop/store/suitable-beauty-tools/8654790995160371815",
  "brand": null,
  "productUrl": "https://shop.tiktok.com/us/pdp/professional-makeup-brush-set-13-soft-fur-brushes-perfect-makeup/1732304214281785959",
  "imageUrl": "https://p16-oec-general-useast5.ttcdn-us.com/tos-useast5-i-omjb5zjo8w-tx/1f4954883fb143619c8fa9928ea79b4e~tplv-fhlh96nyum-crop-webp:800:800.webp",
  "images": [
    "https://p16-oec-general-useast5.ttcdn-us.com/tos-useast5-i-omjb5zjo8w-tx/1f4954883fb143619c8fa9928ea79b4e~tplv-fhlh96nyum-crop-webp:800:800.webp",
    "https://p19-oec-general-useast5.ttcdn-us.com/tos-useast5-i-omjb5zjo8w-tx/1f4954883fb143619c8fa9928ea79b4e~tplv-fhlh96nyum-crop-webp:800:800.webp"
  ],
  "promotionLabels": ["Free shipping"],
  "stockStatus": null,
  "searchTerm": "makeup brush",
  "sourceType": "search",
  "sourceUrl": "https://www.tiktok.com/shop/s/makeup%20brush",
  "region": "US",
  "scrapedAt": "2026-08-16T18:14:43.486Z"
}
```

#### Field notes

- `price` is what's actually being charged now, as a number. `priceFormatted` is the same figure with the symbol the site shows.
- `originalPrice` is the crossed-out "was" price. Null when the product isn't discounted, which is most of them.
- `discountPercent` is the percentage off as a number rather than the `"59%"` string the page renders. Null when there's no discount.
- `rating` is stars out of five. A brand-new listing with no reviews comes back as null rather than 0.
- `reviewCount` is how many reviews the product has. 0 is a real answer for a new listing.
- `soldCount` is lifetime units sold as TikTok reports it on the card, as a number. Null on a card that doesn't carry one.
- `shopName` is the seller's shop name. `sellerId` and `shopUrl` on the same row let you jump to the whole catalogue, and you can feed that `shopUrl` back in as a `startUrls` entry.
- `productUrl` is the canonical product page, with the slug the site itself uses.
- `images` holds every image URL the listing card carries. `imageUrl` is the first, which is the one the site shows.
- `promotionLabels` holds the promotion chips on the card, like `Free shipping` or `Flash sale`. An empty array means the card carried none.
- `stockStatus` is set only where the listing itself publishes a stock signal, otherwise null. See the limits.
- `searchTerm` says which of your terms or URLs produced the row, so a multi-target run stays sortable.
- `region` is the storefront the row came from, as a two-letter country code.

Real rows carry `"charged": true`. Sample rows carry `"_sample": true`, diagnostic rows carry `"_diagnostic": true` and an `errorCode`. Neither is billed.

### How it works

It reads TikTok Shop's own product feed directly, the same data the product grid is filled with, instead of rendering the page. No headless browser and no login.

Each target opens its own page once to pick up the guest session the site hands any first-time visitor, then asks for products a hundred at a time. That keeps the round trips at roughly one per hundred rows. Whatever the page already rendered server-side is read out of the page and costs no extra request.

Requests leave through a rotating pool of addresses in the storefront's own country, because TikTok Shop decides which catalogue you see from where the request comes from. A thin or empty answer is treated as a throttled address rather than an empty catalogue: the run moves to a fresh address and asks again before it will accept "there is nothing here".

### Shop pages behind a bot check

TikTok serves some shop pages behind a bot check to every logged-out visitor. Not to this Actor in particular, and not because of where the request comes from.

When that happens the run looks the shop up by name through search and keeps the products belonging to that seller, which usually recovers most of the shelf. Measured over 75 store reads: 72 came back with products, 3 did not. Search terms and collection URLs aren't affected. Those returned products on every one of 125 reads. If nothing at all comes back you get one uncharged `BLOCKED` row and pay nothing for the attempt.

If you'd rather the run retried such a page with a signed-in reader, put your own TikTok `sessionid` cookie into `sessionCookies`. It's optional and most people should leave it empty. A cookie is as good as a password for the account it belongs to, so use an account you're comfortable exposing, and expect it to stop working when you log out of that account.

### What people use it for

- Product research. Pull a category, sort by units sold, see what's actually moving before you commit to stocking it. Rating and review count sit on the same row, so you can filter out the one-review listings.
- Shelf tracking. Feed in a store URL and get that seller's whole shelf with prices, discounts and sold counts. Re-run on a schedule and diff on `productId`.
- Price monitoring. `price`, `originalPrice` and `discountPercent` are numbers, so a daily run turns straight into a price history without any parsing.
- Affiliate sourcing. Find high-sold, well-rated products in a niche and jump to the shop behind each through `shopUrl`.
- Promo research. `promotionLabels` shows which listings are running flash sales right now, which is a decent proxy for what a seller is pushing.

### Reading the output

- **Real rows** carry `"charged": true` and `"recordType": "product"`. One billed event each.
- **The sample row** carries `"_sample": true` and `"charged": false`. There's exactly one, only when the input had no terms and no URLs.
- **Diagnostic rows** carry `"_diagnostic": true`, `"charged": false` and an `errorCode`: `NO_RESULTS` when a term or shop returned nothing, `NOT_FOUND` when the URL or seller id doesn't exist, `BAD_INPUT` when a URL isn't a TikTok Shop page this Actor understands, `NETWORK` when the target couldn't be reached, `TIME_BUDGET` when time ran out. Each carries plain-English text and the target it belongs to.

Filter on `charged == true` and you have exactly the rows you paid for.

### Which URLs work

| You paste | What you get |
|---|---|
| `https://shop.tiktok.com/us/k/probiotic-sodas` | the collection page's products, paged until `maxItems` |
| `https://www.tiktok.com/shop/store/bk-beauty-brushes/7495182121564604692` | that shop's catalogue, in the shop's own order |
| `https://www.tiktok.com/shop/s/makeup%20brush` | the same as putting `makeup brush` in `searchTerms` |
| `7495182121564604692` | a bare seller id, treated as a store |

A single product page URL isn't a valid target. This returns lists of products, and a product page would be a list of one. Paste the shop URL from the product page instead, or search for the product name.

### Limits

- This reads the **US** storefront. TikTok decides which catalogue to serve from where the request comes from, and the addresses this Actor uses land in the US. For another storefront, supply your own proxy servers exiting in that country through `proxyUrls`. The `region` field on every row tells you which storefront actually answered.
- The per-unit inventory count isn't published on the product listing surface, so `stockStatus` is null for most rows. It's filled in only where the listing itself says something definite.
- A single product page URL isn't a supported target.
- `sortBy` applies to search terms and collection URLs. A store page comes back in the order the shop arranges its own shelf, which the site doesn't let you re-sort.
- Prices, ratings, review counts and sold counts are a snapshot at read time. They move, sometimes hourly on a flash sale.
- Search depth isn't unlimited. TikTok stops handing out more pages for a term after a few hundred products, and when it does the run stops rather than repeating rows. Narrow the term, or add more terms, to go wider.
- `brand` is only present when the seller filled it in, which is roughly four listings in ten.
- Product descriptions, variant and SKU prices, shipping cost, delivery estimates and individual reviews aren't part of a listing row.
- Hard ceilings: 5,000 rows and 20 targets per run.
- Image URLs are TikTok CDN links carrying an expiry, so download anything you want to keep rather than hot-linking it.

### Questions

**What happens if a term matches nothing?**

One uncharged diagnostic row for that target with `errorCode: "NO_RESULTS"`, and the run carries on.

**Do I need a TikTok account, a cookie or an API key?**

No. There's no API key anywhere in this and you don't have to supply a cookie. Runs read the same public feed a logged-out visitor sees.

**So what is `sessionCookies` for?**

The minority of shop pages TikTok puts behind a bot check for logged-out visitors, plus a rate limit nobody else shares if you're running heavily. Optional, and most people should leave it empty.

**Do I need a proxy?**

No. The run brings its own egress and its cost is already inside the price. `proxyUrls` exists only for callers who want traffic to leave through servers they already own, for example to reach a storefront in another country.

**Will the run fail if something goes wrong?**

No. A blocked, empty or broken target produces an uncharged diagnostic row and the run still finishes as succeeded. A failed run would still bill the start fee, which would mean paying to be told something went wrong.

**How do I get the best sellers in a category?**

Put the category as a search term, or paste a collection URL, set `sortBy` to `best_sellers`, and read the `soldCount` column. That's the ordering TikTok itself uses for its ranking pages.

**Can I run it on a schedule?**

Yes. Nothing is held between runs. Use `productId` to work out which rows are new and to diff prices since last time.

# Actor input Schema

## `searchTerms` (type: `array`):

Keywords to search TikTok Shop for, for example "makeup brush" or "wireless earbuds". Up to 20 targets per run in total, counting the URLs below. Each term is searched in turn until the run reaches the row limit.

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

TikTok Shop pages to read instead of, or as well as, search terms. Three shapes are understood: a collection page (https://shop.tiktok.com/us/k/probiotic-sodas), a store page (https://www.tiktok.com/shop/store/bk-beauty-brushes/7495182121564604692) and a search page (https://www.tiktok.com/shop/s/makeup%20brush). A bare seller id also works.

## `sortBy` (type: `string`):

How TikTok orders the products it returns for a search term or a collection URL. Store pages always come back in the shop's own order. "relevance" is what the site shows by default.

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

Total number of products to return across all search terms and URLs. The budget is shared evenly between the targets, so four targets and 100 rows gives you 25 of each. Keep it low while you are testing - you pay per product.

## `proxyUrls` (type: `array`):

Leave this empty. By default the run rotates a large pool of US addresses that cost you nothing per gigabyte. Fill it in only if you specifically want the traffic to leave through proxy servers you already pay for, in the form http://user:pass@host:port. They must exit in a country where TikTok Shop operates.

## `sessionCookies` (type: `array`):

Optional. Leave this empty. TikTok Shop search, collection and store pages are read without any account and that is the normal path. Supply your own TikTok account cookie only if you are running heavily and want a rate limit nobody else shares. In Chrome: F12 -> Application -> Cookies -> tiktok.com, and copy the value of sessionid (a whole "sessionid=...; sid\_tt=..." cookie string is also accepted). Anyone with this value can act as your account, so treat it like a password.

## Actor input object example

```json
{
  "searchTerms": [
    "makeup brush",
    "wireless earbuds"
  ],
  "startUrls": [],
  "sortBy": "relevance",
  "maxItems": 50
}
```

# Actor output Schema

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

Every row in the default dataset: productId, title, price, currency, originalPrice, discountPercent, rating, reviewCount, soldCount, shopName, productUrl, imageUrl, stockStatus, searchTerm, sellerId, shopUrl, brand, priceFormatted, promotionLabels, images, sourceType, sourceUrl, region, scrapedAt. An empty, blocked or unmatched run returns a single uncharged row explaining what happened instead.

# 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 = {
    "searchTerms": [
        "makeup brush",
        "wireless earbuds"
    ],
    "startUrls": [],
    "sortBy": "relevance",
    "maxItems": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/tiktok-shop-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 = {
    "searchTerms": [
        "makeup brush",
        "wireless earbuds",
    ],
    "startUrls": [],
    "sortBy": "relevance",
    "maxItems": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/tiktok-shop-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 '{
  "searchTerms": [
    "makeup brush",
    "wireless earbuds"
  ],
  "startUrls": [],
  "sortBy": "relevance",
  "maxItems": 50
}' |
apify call dami_studio/tiktok-shop-scraper --silent --output-dataset

```

## MCP server setup

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