# TikTok Shop Scraper - Products, Prices, Sales & Reviews (US) (`lanternlab318/tiktok-shop`) Actor

Search TikTok Shop US by keyword or paste product links for public product data: title, price and original price, discount, units sold, rating, review count, shop, images and URL - plus the product page's public reviews. No login, no cookies. Anything cut short is labelled, never reported as zero.

- **URL**: https://apify.com/lanternlab318/tiktok-shop.md
- **Developed by:** [Lantern Lab](https://apify.com/lanternlab318) (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

from $1.99 / 1,000 products

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 Shop Scraper — Products, Prices, Sales & Reviews (US)

A Lantern Lab Actor. Give it TikTok Shop keywords or product links and get back each product's **title, price and original price, discount, units sold, rating, review count, shop, images and product URL** — and, if you switch reviews on, the **reviews the product page shows publicly**, with rating, text, post date and country. United States storefront. No TikTok login, no cookies, no account of yours.

**Price:** you pay per product row and per review row. Keywords that return nothing, dead links, status rows, the run summary and any product row TikTok Shop served with the price masked are free, and there is no run-start fee.

### What you get

- **Products** from a keyword search, in result order, or from product links / bare product IDs.
- **price** and **originalPrice** with **discount** and **currency**. For a product sold in a range, `price` is the low end and `priceMax` the high end. `originalPrice` is only the struck-through price the page actually shows; a product on sale at its list price gets `null`, not a made-up "was" figure. When TikTok Shop masks the price for logged-out visitors — it shows `$7** - 8**` on some products — `price` is `null`, `priceHidden` is `true`, the row is marked partial and you are not charged for it. It is never a guess and never 0.
- **soldCount**, **rating**, **reviewCount**, **shopName** with shop ID and shop rating, and the **product URL** TikTok Shop itself publishes.
- **imageUrls** — every product image from a product link, the result-list image from a keyword.
- **Reviews** (optional): `rating`, `text`, `publishedAt`, `reviewerCountry`, verified-purchase flag, the review's own photos and the display name the page shows — TikTok Shop masks that itself (`L**`), and the reviewer's account ID and avatar are left alone. Each review is linked to its product by `productId`.
- **Clear status for every input.** A keyword that returned nothing, a product ID TikTok Shop does not have, and an input cut short by a limit are three different, labelled records — never confused, and a blocked request is never reported as zero results.
- **No duplicates.** A product found by several keywords (or by a keyword and a link) is written and charged once; so is a review.
- **Two platform-specific timestamps kept apart.** `collectedAt` is when this run read the page; `publishedAt` on a product row is always `null`, because TikTok Shop publishes no listing date. On a review row `publishedAt` is the platform's own post time.
- **A hard request ceiling.** `maxUpstreamRequests` caps the run; leave it empty and the Actor derives one from your input.

### Input example

```json
{
  "searchQueries": ["coffee grinder", "air fryer"],
  "productUrls": ["https://www.tiktok.com/shop/pdp/coffee-grinder-electric/1732083256138699473"],
  "region": "US",
  "maxProducts": 30,
  "includeReviews": false
}
```

| Field | Default | Meaning |
| --- | --- | --- |
| `searchQueries` | — | One keyword per line. Use this, `productUrls`, or both. |
| `productUrls` | — | Product links or bare product IDs. `shop.tiktok.com` links and links without the slug work too. |
| `region` | `US` | Storefront to read. This version supports `US` only. |
| `maxProducts` | `30` | Products to take per keyword, from the top of the result page. |
| `includeReviews` | `false` | Also return the product page's public reviews, one row each. |
| `maxReviewsPerProduct` | `5` | Reviews per product. TikTok Shop shows at most 5 publicly. |
| `maxUpstreamRequests` | derived | Hard ceiling on requests to TikTok Shop for the whole run, retries included. |
| `proxyConfiguration` | Apify Proxy | Datacenter is the default and what this Actor is measured on. |

### Output example

A product row (`recordType: "product"`):

```json
{
  "recordType": "product",
  "productId": "1732083256138699473",
  "title": "coffee grinder, Spice Grinder Electric with Stainless Steel Blades",
  "price": 16.89,
  "priceMax": null,
  "originalPrice": 25,
  "discount": "-32%",
  "currency": "USD",
  "priceHidden": false,
  "priceText": "$16.89",
  "soldCount": 85,
  "rating": 4.5,
  "reviewCount": 11,
  "shopName": "REDSUNSHOP",
  "shopId": "7495952906928491217",
  "shopRating": 4.2,
  "region": "US",
  "imageUrls": ["https://p16-oec-general-useast5.ttcdn-us.com/..."],
  "skuCount": 1,
  "stockTotal": 499,
  "availability": "default",
  "url": "https://www.tiktok.com/shop/pdp/coffee-grinder-electric/1732083256138699473",
  "publishedAt": null,
  "fieldSource": "product_url",
  "sourceType": "product_url",
  "sourceQuery": null,
  "sourceProductUrl": "https://www.tiktok.com/shop/pdp/coffee-grinder-electric/1732083256138699473",
  "rank": null,
  "partial": false,
  "partialReason": null,
  "collectedAt": "2026-10-24T03:12:44.182Z"
}
```

A review row (`recordType: "review"`):

```json
{
  "recordType": "review",
  "reviewId": "7647381888391808781",
  "productId": "1729385034780414637",
  "rating": 5,
  "text": "Great product. Easy setup. Fast shipping.",
  "publishedAt": "2026-04-08T09:57:48.000Z",
  "reviewerCountry": "US",
  "isVerifiedPurchase": true,
  "imageUrls": ["https://p16-oec-general-useast5.ttcdn-us.com/..."],
  "nickname": "L**",
  "helpfulCount": null,
  "productUrl": "https://www.tiktok.com/shop/pdp/1729385034780414637",
  "partial": false,
  "partialReason": null,
  "collectedAt": "2026-10-24T03:12:48.903Z"
}
```

Every keyword also gets one free `query_status` row and every link one free `product_status` row, and the last row of the dataset is `RUN_SUMMARY` (also in the key-value store under `RUN_SUMMARY`): products and reviews collected, how many of them were charged, duplicates skipped, products with a hidden price, which inputs are incomplete and why, how many requests went to TikTok Shop, and whether the run stopped early.

### Limits — read these before you run

1. **30 products per keyword.** TikTok Shop's public search page serves one page of results and ignores the paging parameters in the URL, so a keyword cannot return more than about 30 products however high you set `maxProducts`. It also renders that page at two sizes for the same keyword — 25-30 results, or 5 that claim to be complete — so the Actor asks again from a new IP while the page comes back short and keeps the largest rendering; `searchAttempts` on the keyword's status row says how many it needed. The status row also says when the page held more than you asked for (`max_products_reached`) and when TikTok Shop itself said there is more behind a cursor only its own app can follow (`platform_search_limit`). Use several narrower keywords, or product links, to go wider.
2. **5 reviews per product.** The public product page shows only its first screen of reviews. A product with more gets a `reviews_status` row saying how many of how many arrived (`platform_review_limit`). Reading every review of a product is not possible without logging in, which this Actor does not do.
3. **TikTok Shop has no "no results" page for a keyword.** Search for something nothing matches and it fills the result list with unrelated recommendations under a "Results for …" heading. This Actor returns what the page holds rather than inventing an empty result, so check the titles before trusting a very narrow keyword.
4. **Some prices are hidden from logged-out visitors.** Those rows carry `priceHidden: true`, `price: null` and `partial: true`, and everything else the page published — and they are **free**: a row missing a field this Actor promises is not charged, and it does not consume your run's maximum charge either. `RUN_SUMMARY` reports them as `productsWithHiddenPrice`, and `productsCharged` excludes them. Which products those are is decided per visit, not per product: the same product can answer with a price on one read and a masked one on the next.
5. **A price is what TikTok Shop showed at `collectedAt`, not a stable attribute.** TikTok Shop runs per-session promotions, so the same product can read `$59.99`, `$27.99` and `$31.43` within a quarter of an hour, and a product row from a keyword can differ from the same product's own page read minutes later. `price` is always the figure the page carried on the request this row came from; `fieldSource` says which page that was. Re-read a product rather than treating one row as its list price.
6. **Occasional security checks.** TikTok Shop answers some requests with a captcha page instead of the data. Each retry leaves from a new IP, and a request that keeps being refused is reported as `blocked` on the input's status row — never as an empty result. If a run sees many of them, switch `proxyConfiguration` to residential.
7. **United States only.** Other storefronts are refused at input validation rather than returning a US catalogue under the wrong label.
8. **Public pages only.** This Actor reads the same pages a logged-out visitor sees, at `/shop/pdp/…` and `/shop/s`, and it does not read anything TikTok's `robots.txt` disallows. From a review it takes only what the page shows: the already-masked display name, rating, text, date, country and the review's own photos — not the reviewer's account ID and not their avatar.

# Actor input Schema

## `searchQueries` (type: `array`):

One TikTok Shop keyword per line: "coffee grinder", "yoga mat", "air fryer". Each keyword is looked up separately and every product row says which keyword found it. TikTok Shop's public search page serves one page of results per keyword and offers no way to page past it, so a keyword returns at most 30 products however high you set maxProducts - and because it sometimes renders that page with only 5 results while claiming to be complete, the Actor asks again from a new IP while the page comes back short and keeps the largest rendering. A product found by several keywords is written and charged once.

## `productUrls` (type: `array`):

One TikTok Shop product page per line: "https://www.tiktok.com/shop/pdp/<slug>/<product-id>". A "https://shop.tiktok.com/us/pdp/..." link, a link without the slug or the bare product ID also work. Each link is read from the product's own page, which carries more than the search list does: every product image, the stock state and the shop's rating.

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

Which TikTok Shop storefront to read. Only the United States storefront is supported in this version; other regions are refused up front rather than returning a US catalogue under the wrong label.

## `maxProducts` (type: `integer`):

Upper bound on the product rows one keyword may produce. It applies per keyword, not to the whole run: three keywords at 30 can return 90 products. Product links are not affected - every link you give is read. TikTok Shop's own page limit is 30 products per keyword, so a higher number cannot return more and the keyword is marked partial when it is reached.

## `includeReviews` (type: `boolean`):

Collect the reviews TikTok Shop shows on the public product page as well. Off by default, because it costs one extra request per product. The product page shows only its first screen of reviews - at most 5 - and offers no public way to page past them, so a product with more reviews than that gets a review status row saying how many of how many arrived. Only what the page shows publicly is collected: rating, text, post date, country, whether it is a verified purchase, the review's own photos, and the display name TikTok Shop itself publishes already masked ("L\*\*"). The reviewer's account ID and avatar are not collected.

## `maxReviewsPerProduct` (type: `integer`):

Upper bound on the review rows one product may produce. TikTok Shop's public product page serves at most 5 reviews, so a higher number cannot return more.

## `maxUpstreamRequests` (type: `integer`):

Hard ceiling on the HTTP requests this run sends to TikTok Shop: keyword searches, product pages and review pages together, retries included. At the ceiling collection stops - the inputs not reached are written with partialReason "upstream_request_cap" and the run then ends normally. Leave empty and the Actor works a ceiling out from your keywords, links, maxProducts and includeReviews, so a run is never unbounded.

## `proxyConfiguration` (type: `object`):

Where the requests to TikTok Shop come from. The default (Apify Proxy, datacenter) is the cheapest and is what this Actor is measured on. TikTok Shop answers an occasional request with a security check instead of the page; every retry leaves from a new IP, and a request that keeps being refused is reported as blocked rather than as an empty result. Switch to residential if your run sees many security checks.

## Actor input object example

```json
{
  "searchQueries": [
    "coffee grinder"
  ],
  "productUrls": [],
  "region": "US",
  "maxProducts": 30,
  "includeReviews": false,
  "maxReviewsPerProduct": 5,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

No description

## `runSummary` (type: `string`):

No description

# 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 = {
    "searchQueries": [
        "coffee grinder"
    ],
    "productUrls": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("lanternlab318/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 = {
    "searchQueries": ["coffee grinder"],
    "productUrls": [],
}

# Run the Actor and wait for it to finish
run = client.actor("lanternlab318/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 '{
  "searchQueries": [
    "coffee grinder"
  ],
  "productUrls": []
}' |
apify call lanternlab318/tiktok-shop --silent --output-dataset

```

## MCP server setup

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