# TikTok Shop Product Scraper - Units Sold, Price & Stock (`neverempty/tiktok-shop-product-scraper`) Actor

For TikTok Shop sellers, brands and product researchers: paste product URLs or a store and get exact units sold, sale and list price, rating, reviews, stock per variant and the store. Monitoring mode returns only products whose sales or price changed since the last run.

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

## Pricing

from $9.00 / 1,000 product row returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

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 Shop Product Scraper - Units Sold, Price & Stock

For TikTok Shop sellers, brands and product researchers: paste TikTok Shop product URLs, product ids or a store, and get each product's exact units sold, sale and list price, discount, rating and review count, stock per variant, shipping and the store behind it. TikTok shows rounded counts such as "1.3M sold"; this Actor returns the whole number from the product page (1,305,218 for Goli's ashwagandha gummies on 2026-10-19). Schedule it in monitoring mode and get back only the products whose units sold or price changed since the last run, with how many units sold in between.

Export as JSON, CSV or Excel. Unofficial. Public data only.

### What you get

- **Exact units sold per product**, read from the product page itself, not from a search card. Across 38 US product pages read on 2026-10-19, every product returned was the product asked for, and every store matched the store that sells it.
- **Sale price and list price for the cheapest variant**, the discount percent TikTok shows, and the lowest and highest price across variants (`priceMin`, `priceMax`).
- **Stock per variant**: each variant's name, options (size, color, flavor), sale price, list price, units in stock and whether it can be bought. `inStock` and `variantsInStock` sum it up per product.
- **Rating, review count and the 5-to-1 star breakdown.**
- **The store**: id, name, link, rating, followers, number of products and the store's total units sold, on every row.
- **Shipping**: fee, free shipping, and the delivery window in days.
- **Images, description, specifications, brand and the full category path.**
- **Whole stores, best-sellers first.** Put a store URL in `stores` and the Actor reads its products in TikTok's best-selling order, up to `maxProductsPerStore`, each from its own product page (Goli Nutrition: all 52 products on sale).
- **Monitoring mode** returns a product only when its units sold or price changed, with `unitsSoldSincePrevious` and the hours between the two readings, so a schedule gives you daily sales per product.
- **Runs that finish.** A product TikTok does not return comes back as a free row that says why, and the run goes on to the next one. Nothing is guessed: a value that is not on the page is `null`.

### Input

| Field | What it does |
|---|---|
| `products` | Product URLs (`https://shop.tiktok.com/us/pdp/<name>/<id>`, `https://www.tiktok.com/shop/pdp/<name>/<id>`) or numeric product ids. Commas or new lines separate entries. A store URL here is read as a store. |
| `stores` | Store URLs (`https://shop.tiktok.com/us/store/<name>/<id>`) or numeric store ids. The store's products are read best-selling first. |
| `maxProductsPerStore` | How many products of each store are read (default 20, up to 500). Has no effect on `products`. |
| `monitoringMode` | Return a product only when it changed since the row last returned for it. |
| `changeOn` | What counts as a change: `units-sold-or-price` (default), `units-sold` or `price`. |
| `resetMonitoringState` | Forget the remembered values; the next monitoring run returns every product once again. |

Up to 500 entries per run (products and stores together). With both lists empty and monitoring off, two example products are read.

```json
{
    "products": [
        "https://shop.tiktok.com/us/pdp/ashwagandha-gummies-by-goli/1729527313880355335",
        "1732164626458252048"
    ],
    "stores": ["https://shop.tiktok.com/us/store/goli-nutrition/7495794203056835079"],
    "maxProductsPerStore": 20
}
```

### Output

One row per product. Example (shortened: one variant, one image, the description cut):

```json
{
    "source": "tiktok-shop-product",
    "status": "ok",
    "scrapedAt": "2026-10-19T09:12:00.000Z",
    "inputUrl": "https://shop.tiktok.com/us/pdp/1731236289885213002",
    "foundVia": "product",
    "rankInStore": null,
    "region": "us",
    "productId": "1731236289885213002",
    "productUrl": "https://shop.tiktok.com/us/pdp/1731236289885213002",
    "title": "SEESE Cordless Pressure Washer Gun, 1000 PSI Portable Electric Washer for Vehicle, Driveway & Deck ...",
    "brand": "SEESE",
    "category": "Water Guns & Snow Foam Lances",
    "categoryPath": "Automotive & Motorcycle > Car Washing & Maintenance > Water Guns & Snow Foam Lances",
    "categoryId": "945160",
    "unitsSold": 518459,
    "currency": "USD",
    "salePrice": 55.37,
    "listPrice": 99,
    "isDiscounted": true,
    "discountPercent": 44,
    "priceMin": 55.37,
    "priceMax": 79,
    "rating": 4.2,
    "reviewCount": 35279,
    "ratingBreakdown": { "fiveStar": 22699, "fourStar": 4046, "threeStar": 2639, "twoStar": 1477, "oneStar": 4418 },
    "inStock": true,
    "variantCount": 3,
    "variantsInStock": 3,
    "variantOptions": { "Water Pressure": ["1000PSl-Water Pressure", "500PSl-Water Pressure", "300PSl-Water Pressure"] },
    "variants": [
        { "skuId": "1731985915991724362", "name": "1000PSl-Water Pressure", "options": { "Water Pressure": "1000PSl-Water Pressure" },
          "salePrice": 55.37, "listPrice": 99, "currency": "USD", "stock": 486, "available": true, "soldOutText": null }
    ],
    "shippingFee": 0,
    "freeShipping": true,
    "deliveryMinDays": 3,
    "deliveryMaxDays": 6,
    "storeId": "7495813187623356746",
    "storeName": "SEESE Store",
    "storeUrl": "https://shop.tiktok.com/us/store/seese-store/7495813187623356746",
    "storeRating": 4.3,
    "storeFollowerCount": 45482,
    "storeProductCount": 71,
    "storeUnitsSold": 1987991,
    "imageUrls": ["https://p16-oec-general.ttcdn-us.com/..."],
    "hasVideo": true,
    "description": "For safety and service life, please fully charge before first use ...",
    "specifications": [{ "name": "Brand", "value": "SEESE" }, { "name": "Battery power (Wh)", "value": "200" }],
    "change": null,
    "isFirstCheck": null,
    "previousCheckedAt": null,
    "previousUnitsSold": null,
    "unitsSoldSincePrevious": null,
    "hoursSincePrevious": null,
    "previousSalePrice": null,
    "previousPriceMax": null
}
```

| Column | Meaning |
|---|---|
| `unitsSold` | Whole-number units sold that TikTok publishes for the product. |
| `salePrice` / `listPrice` | Price and pre-discount price of the cheapest variant. `listPrice` equals `salePrice` when there is no discount. |
| `priceMin` / `priceMax` | Lowest and highest sale price across variants. |
| `discountPercent` | The discount TikTok shows for the product. |
| `variants[].stock` | Units in stock for that variant as TikTok publishes it. `available` is false for a sold-out variant. |
| `foundVia` / `rankInStore` | `product` for the product list; `store` with the best-selling position for products read from a store. |
| `storeUnitsSold` | The store's total units sold. |

### Monitoring mode

Turn on `monitoringMode` and run the Actor on a schedule:

1. **First run:** every product comes back once (`change`: `first-check`) to set the baseline. These are normal product rows; no check fee.
2. **Later runs:** a product comes back only when it changed since the row last returned for it: `units-sold-changed` or `price-changed`, with `previousUnitsSold`, `unitsSoldSincePrevious`, `previousSalePrice`, `previousPriceMax`, `previousCheckedAt` and `hoursSincePrevious`.
3. A product that did not change is not returned and costs only the small check fee. If nothing changed, the run returns one free `no-change-since-last-run` row.

A product that is checked but not returned keeps the values of the row last returned for it, so the `unitsSoldSincePrevious` values of the rows you receive add up to the units sold over the whole period.

Values are remembered per product in a named key-value store of your account (`tiktok-shop-product-monitoring`). A product that was not returned because the run reached its maximum total charge is not remembered, so it comes back on the next run.

### Pricing

Pay per event (prices on the Pricing tab):

- **`product-returned`**: each product row in the dataset.
- **`product-checked`**: monitoring mode only, each product that was read and had not changed (no row is returned for it).
- Rows with a `status` other than `ok` are free.
- The Actor reads a product only when the run's maximum total charge still has room for its row, so a low limit ends the run early with a free `budget-reached` row instead of fees for pages that cannot be delivered.

### Free rows

Every row with a `status` other than `ok` is free and has a `note` that says why.

| status | Meaning |
|---|---|
| `security-check` | TikTok answered with its "Security Check" page. TikTok gives the same page for an id that does not exist, so the row does not say whether the product exists. The check is not bypassed: no proxy, no retry. |
| `page-not-returned` | TikTok answered with HTTP 404. |
| `product-not-returned` | TikTok answered with an error such as "Product ID is invalid" (a US product id asked under another region does this). |
| `no-product-data` | The page has no product data for this id. |
| `different-product-returned` | TikTok answered with another product than the one asked for; its numbers are not returned as this product's. |
| `no-units-sold` | The page has no whole-number units-sold count, so no row is sold for it. |
| `no-store-data` | The store page has no store data for this store id in this region. |
| `different-store-returned` | TikTok answered with another store than the one asked for. |
| `no-products-listed` | The store has no products in its list. |
| `more-products-not-read` | The request for the store's next products failed; the products already listed are still returned. |
| `blocked` | HTTP 403, 429 or 503, or an empty answer, after waiting and trying again. |
| `unreadable` | The page could not be read. |
| `invalid-input` | Not a TikTok Shop product or store URL or id. |
| `duplicate` | The same product or store as an earlier entry in the run. |
| `no-change-since-last-run` | Monitoring mode: no checked product changed. |
| `budget-reached` | The run's maximum total charge has no room for another row; the row says how many entries were not read. |
| `not-checked` | Entries beyond the first 500 of the list. |

### Good to know

- **Only US products and stores have been checked** (2026-10-19). Other regions' URLs are accepted; a US product id asked under another region comes back as a free `product-not-returned` row.
- Pages are read one at a time with a short pause, so a large list takes a while: about 3 seconds per product (113 products in 6 minutes on 2026-10-19).
- TikTok sometimes answers a real product page with its "Security Check" page: in two runs of 113 products each on 2026-10-19, 5 and 2 products came back as free `security-check` rows. Run those products again later: 4 of those 6 products returned their data when run again a few minutes later.
- TikTok publishes stock per variant. Variants of one product can share one stock pool, so the numbers are not summed into a product total.
- No fully sold-out product was among the 38 checked on 2026-10-19; sold-out variants (`available`: false, `stock`: 0) were. A product that is entirely sold out is returned with whatever its page shows.
- `unitsSold` is the number TikTok publishes. This Actor does not know how TikTok counts returns or cancellations.
- Image links are TikTok's CDN addresses. Download the images you want to keep.
- TikTok's robots.txt for www.tiktok.com lists `/shop/view/product/`; this Actor reads the public product pages on shop.tiktok.com. You are responsible for how you use the data.
- Do not put the same product in two monitoring schedules that can run at the same time: the Actor re-reads and merges before writing, but that cannot fully prevent one run from overwriting the other's records.

### Thanks for using this Actor

We build these tools for people who run them every day, and we improve them from what users tell us.

- **Missing a field, or need another filter?** Tell us in the **Issues** tab. If the data is there, we add it.
- **Found a bug or a wrong value?** Post the run ID in the **Issues** tab. Wrong data is the thing we fix first.

If this Actor saved you time, a short review helps other people find it.

# Actor input Schema

## `products` (type: `array`):

Products to read: https://shop.tiktok.com/us/pdp/<product-name>/<product-id>, https://www.tiktok.com/shop/pdp/<product-name>/<product-id> (with or without the scheme), or the numeric product id (read as a US product). The product name part of the URL can be anything; the product id decides the product. A store URL pasted here is read as a store. A repeated product is read once and the repeat gets a free 'duplicate' row. Up to 500 entries per run (products and stores together). If you leave both lists out with monitoring off, two example products are read; with monitoring on, at least one list is required.

## `stores` (type: `array`):

Stores whose best-selling products to read: https://shop.tiktok.com/us/store/<store-name>/<store-id>, https://www.tiktok.com/shop/store/<store-name>/<store-id>, or the numeric store id (read as a US store). The store's products are listed by units sold, highest first, and each one is read from its product page like the products above. A product that is also in the product list is read once.

## `maxProductsPerStore` (type: `integer`):

How many products of each store are read, best-selling first. Each product read is one row. Has no effect on the product list.

## `monitoringMode` (type: `boolean`):

Off = every product comes back with its current numbers, charged per row. On = the Actor remembers each product's units sold and price and, on later runs, returns a product only when it changed since the row last returned for it, with the units sold in between and the hours between the two readings. The first run returns every product once to set the baseline (charged as rows, with no check fee). On later runs, a product that did not change is not returned and costs only the small per-product check fee shown on the Pricing tab. The Actor reads only as many products as the run's maximum total charge can pay a row for. Values are remembered per product; do not put the same product in two schedules that can run at the same time.

## `changeOn` (type: `string`):

units-sold-or-price = its units sold or its price changed (the default). units-sold = only its units sold changed. price = only its lowest or highest price changed. A product that is not returned keeps the values of the row last returned for it, so the next row's change is counted from that row.

## `resetMonitoringState` (type: `boolean`):

Clears every remembered value for this Actor, so the next monitoring run returns each product once again as a first check. This affects all your monitoring runs, because values are stored per product rather than per list. Turn it off again after one run: left on in a schedule, every run returns every product as a first check and charges the row price for it.

## Actor input object example

```json
{
  "products": [
    "https://shop.tiktok.com/us/pdp/1729527313880355335",
    "https://shop.tiktok.com/us/pdp/1732164626458252048"
  ],
  "maxProductsPerStore": 20,
  "monitoringMode": false,
  "changeOn": "units-sold-or-price",
  "resetMonitoringState": false
}
```

# Actor output Schema

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

One row per TikTok Shop product: title, URL, brand, category path, exact units sold, sale price, list price, discount, price range, rating, review count and star breakdown, every variant with its price and stock, shipping, the store (id, name, URL, rating, followers) and image URLs. In monitoring mode also the units sold and price of the row last returned. Security checks, invalid input and budget stops come back as free rows that say why.

# 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 = {
    "products": [
        "https://shop.tiktok.com/us/pdp/1729527313880355335",
        "https://shop.tiktok.com/us/pdp/1732164626458252048"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/tiktok-shop-product-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 = { "products": [
        "https://shop.tiktok.com/us/pdp/1729527313880355335",
        "https://shop.tiktok.com/us/pdp/1732164626458252048",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/tiktok-shop-product-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 '{
  "products": [
    "https://shop.tiktok.com/us/pdp/1729527313880355335",
    "https://shop.tiktok.com/us/pdp/1732164626458252048"
  ]
}' |
apify call neverempty/tiktok-shop-product-scraper --silent --output-dataset

```

## MCP server setup

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