# TikTok Shop Scraper - Search, Prices & Reviews (`s-r/tiktok-shop`) Actor

Search TikTok Shop by keyword or browse its categories. Returns title, price, discount, brand, shop, rating with review count, units sold and promotion badges, plus the description and seller stats on product pages.

- **URL**: https://apify.com/s-r/tiktok-shop.md
- **Developed by:** [SR](https://apify.com/s-r) (community)
- **Categories:** E-commerce, Social media
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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 - Search, Prices & Reviews

Read TikTok Shop as structured data: title, price, the pre-promotion price and
discount where one is running, currency, brand, shop, rating with its review
count, units sold, and the promotion badges.

Paste category or product links, or run it with nothing and it reads six of the
storefront's largest categories.

### What you get per product

- `title`, `url`, `product_id`, `slug`
- `price`, `currency`, `currency_symbol`
- `original_price`, `discount_percent`, `saving` — when a promotion is running
- `rating` out of 5 and `review_count`
- `sold_count` — units sold, which is the number most catalogues never publish
- `brand`, `seller`, `seller_id`
- `promotion_labels` such as `Flash sale`
- `image_url`, `sku_id`
- `found_on` and `source` — which category, search or product page the row came from

Rows read from a **product page** carry more: the full `description`, the
`category_path`, and the seller's own numbers (`seller_followers`,
`seller_sold_count`, `seller_product_count`). Those come from search results and
from any product you name directly.

**A product with no reviews has no rating.** TikTok reports `0.0` there, which
sorts as the worst item on the page; the field comes back empty instead, with
`review_count: 0` saying why.

`sold_count` is the field worth the run on its own. Rating tells you how a
product was received; units sold tells you whether anyone bought it. A 4.9 on
twelve reviews and a 4.5 on thirty thousand sales are different products.

### An empty discount means no discount

Most products on TikTok Shop are not on promotion, and for those the storefront
publishes a price and nothing else.

This Actor leaves `original_price`, `discount_percent` and `saving` **empty** on
those rows rather than copying the sale price across into the "before" column.
That copy is tempting because it makes the table look complete, and it would
give every ordinary product a 0% discount that reads, once you sort by it, as
though the product had been checked and found not to be on offer. It has not
been checked. There is simply no promotion.

The run summary reports `onPromotion` so you can see how much of a run carried
one. In testing that was 9 of 26.

### Search by keyword

TikTok Shop publishes **no search endpoint**: `/search`, `/us/search` and
`/us/s/term` all return 404, and the storefront offers a category tree and
nothing else.

Type a keyword anyway. The matching product pages are found in a public web
index, and each one is then **read live from TikTok**. Only the finding is
second-hand: every price, rating and sold count in your results comes from
TikTok at the moment of the run, which matters because a search index is weeks
stale on exactly those fields.

`vitamin c serum` resolved to eight products with their live ratings, review
counts and units sold in under a minute.

Give a keyword **and** categories and it works the other way instead: the
keyword filters what the categories serve, and the crawl widens automatically
to six passes and one subcategory level to give the filter something to work
with.

### The redacted prices, which is the thing to know before you trust a column

TikTok **blanks out digits in the price on some product pages**. Not all of
them, and there is no pattern to which:

```
"sale_price_decimal": "2*"      redacted
"sale_price_decimal": "5*"      redacted
"sale_price_decimal": "27.17"   real
```

Read carelessly, `"2*"` becomes `2` and a twenty-nine dollar product appears in
your table at two dollars. That is not a rounding error. It is an order of
magnitude, it looks entirely plausible next to real prices, and it is wrong in
the direction that makes something look like a bargain.

So a redacted price is returned **empty**, with `price_masked: true` on the row
and `pricesRedactedByTikTok` in the run summary. Everything else on that product
survives: the rating, the review count, the units sold, the seller. In a
sample of eight search results, three were redacted.

Category listings are not redacted, so browsing returns prices for everything.

### Re-asking is the pagination

This is the other thing worth knowing, because it is not obvious and it is where
the volume comes from.

A category page shows 15 products, and `?page=2` appears to fetch the next
fifteen. It does not. `?p=2` returns "new" products too, and so does a made-up
`?cursor=15`, because **the list is rotated per request** rather than paged.
Four fetches of the identical URL returned **55 distinct products, not 15**.

So **Passes per category** is the real control over how much of a category you
see:

| passes | distinct products from one category |
|---|---|
| 1 | 15 |
| 3 | 45 |
| 4 | 60 |

Add **Subcategory depth** on top and one category becomes its whole first level:
that same category at depth 1 returned **117 products from 21 subcategories**.

### Ids are enough

Paste a category link, a product link, or just the id. `https://shop.tiktok.com/us/c/-/700645`
serves the same page as the full slug URL, and `/us/pdp/x/<id>` the same as the
canonical product URL, so the slug carries no information and is ignored.

A six-to-nine digit number is read as a category, a longer one as a product.
Anything shorter is refused rather than guessed at.

Leaving the input empty reads six large categories: beauty and personal care,
womenswear, food and beverages, home supplies, health, and phones and
electronics.

### About the retries, because you will see them

The storefront is a single-page app, and asking for a page returns one of two
things. Roughly three times in four you get a small shell with no products in
it, at HTTP 200 with no error and nothing that looks like a refusal. The fourth
time the same URL returns the full server-rendered catalogue.

So pages are simply requested again. **Attempts per page** defaults to 10, which
clears about 90% on the measured rate, and the run summary reports
`requestsRetried` and `retriesPerPage` so the cost of that is visible rather
than hidden.

This is worth knowing for two reasons. A run taking longer than you expect is
usually this, not a fault. And a `retriesPerPage` figure that climbs across
scheduled runs is the early warning that something changed, well before the
Actor starts failing.

A page that never comes through in its budget is reported by URL, with the
number of attempts spent, rather than quietly dropped.

### Filters and scale

- **Categories or products** — links or bare ids, one per line
- **Maximum products** — the ceiling on both time and cost, applied after
  duplicates are removed across pages
- **Attempts per page** — raise it if a run reports pages it could not read
- **Market** — two-letter code, which sets the storefront and the exit country.
  The United States storefront is the one this has been verified against.

A category page carries 15 to 21 products. Three pages and 26 distinct products
took **seven seconds** in a run that needed no retries at all.

### What people use this for

**Trend spotting.** `sold_count` next to `rating` and the promotion badges is
what separates a product being pushed from a product being bought. Run the same
categories daily and the movers are the rows whose sold count jumps.

**Price and promotion monitoring.** Because an empty discount really means no
discount, a run tells you exactly which products went on offer since the last
one, without having to infer it.

**Competitive research.** Brand and shop on every row means a category run
groups directly into who is selling what, at what price, and with how much
volume behind it.

**Sourcing.** Units sold against price is the crude but effective filter for
what a category actually moves.

### Notes

Prices, discounts and sold counts are live and change through the day; a run is
a snapshot.

Products are read from category and product pages, so this returns what the
storefront lists. Reviews are counted but not fetched: a product with 158
reviews gives you the number, not the 158 texts.

A link that is neither a product nor a category is reported by name rather than
silently skipped, so a run of fifty tells you exactly which line was wrong.

# Actor input Schema

## `keyword` (type: `string`):

Search TikTok Shop for this. TikTok publishes no search endpoint, so the matching product pages are found in a public web index and then read live from TikTok: only the finding is second-hand, every price and rating comes from TikTok at run time. Give categories as well and the keyword filters those instead.

## `targets` (type: `array`):

TikTok Shop category URLs or ids (700645), product URLs or ids. A bare six-digit number is read as a category and a long one as a product, and the slug in a URL is ignored because the id is what resolves. Leave empty to read six of the storefront's largest categories.

## `max_products` (type: `integer`):

Stop after this many distinct products. Also the cost ceiling for the run, and the thing to raise before passes when you want more.

## `search_pages` (type: `integer`):

How many pages of search results to resolve, roughly ten products each. Only used when a keyword is given with no categories.

## `passes` (type: `integer`):

How many times to re-request each category. The product list is rotated per request rather than paginated, so asking again is what pages it: four passes of one category returned 55 distinct products where one returned 15. Defaults to 3, or 6 when a keyword is set. Leave empty to let the Actor choose.

## `subcategory_depth` (type: `integer`):

How many levels of subcategories to walk. Each category page lists its own children, so 1 turns a single category into its whole first level. Defaults to 0, or 1 when a keyword is set. Leave empty to let the Actor choose.

## `attempts` (type: `integer`):

The storefront answers with an empty shell roughly three times in four and the same request succeeds on a later try, so pages are retried. Ten attempts clear about 90 percent. Raise it if a run reports pages it could not read.

## `region` (type: `string`):

Two-letter market code, which sets both the storefront and the exit country. The United States storefront is the one this has been verified against.

## Actor input object example

```json
{
  "keyword": "serum",
  "targets": [
    "https://shop.tiktok.com/us/c/health/700645"
  ],
  "max_products": 100,
  "attempts": 10,
  "region": "us"
}
```

# Actor output Schema

## `products` (type: `string`):

One row per product.

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

Pages read, products returned, how many carried a rating and a promotion, and how many requests had to be retried.

## `errors` (type: `string`):

Pages that could not be read.

# 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 = {
    "targets": [
        "https://shop.tiktok.com/us/c/beauty-personal-care/601450"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/tiktok-shop").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 = { "targets": ["https://shop.tiktok.com/us/c/beauty-personal-care/601450"] }

# Run the Actor and wait for it to finish
run = client.actor("s-r/tiktok-shop").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 '{
  "targets": [
    "https://shop.tiktok.com/us/c/beauty-personal-care/601450"
  ]
}' |
apify call s-r/tiktok-shop --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,s-r/tiktok-shop"
        }
    }
}

```

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/SdQKv03hUrtk1nnPW/builds/IUzAmongtfuSkdEXj/openapi.json
