# TikTok Shop Scraper — Exact Sold Counts, No Login (`jurassic_jove/tiktok-shop-scraper`) Actor

Scrape TikTok Shop by keyword and get exact units sold per product — 164, not the rounded badge — plus title, price, seller, image and link. One row per product. No login, no cookies, no proxy needed.

- **URL**: https://apify.com/jurassic\_jove/tiktok-shop-scraper.md
- **Developed by:** [Data Runner](https://apify.com/jurassic_jove) (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

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 — Exact Sold Counts, No Login 🛍️

**Scrape TikTok Shop by keyword and get the number nobody else gives you: the
exact units sold per product.** `164`, not the rounded "164 sold" badge the page
displays. Plus title, price, seller, image and link — one row per product.

No login. No cookies. No captcha solving. Runs from a plain datacenter IP.

> **Why this matters for product research:** on TikTok Shop the winners are
> memes. A single read of `funny shirt` returns *John Pork Tung Tung Tung Sahur
> 67 Shirt* at 1,206 units and *Lollipop Cat Brainrot Tee* — trends that are
> selling right now, priced under $6, from sellers you have never heard of.

***

### What you get — one row per product

```jsonc
{
  "term": "funny shirt",        // exactly as you typed it
  "geo": "US",
  "position": 1,                // the storefront's order on page one
  "productId": "1732481525745357276",
  "title": "Funny graphic tees \"There is Motion at Your Front Door\" Tung Tung Sahur",
  "sold": 164,                  // EXACT, not the rounded badge the page shows
  "price": 4.8,
  "currency": "USD",
  "shop": "A2HMK",
  "image": "https://p16-oec-general-useast5.ttcdn-us.com/…",
  "url": "https://shop.tiktok.com/us/pdp/…/1732481525745357276",
  "competingTotal": null,       // TikTok does not publish it — see FAQ
  "cardsOnPageOne": 30,         // what THIS read returned. Not the field size.
  "scrapedAt": "2026-09-05T19:00:00.000Z",
  "sourceUrl": "https://shop.tiktok.com/us/s?q=funny%20shirt"
}
```

### Who uses this

- **Print-on-demand and merch sellers** — find which meme is selling on shirts
  today, at what price, before it is everywhere.
- **Dropshippers and product researchers** — see the real winners in a category,
  their exact unit counts, and who is selling them.
- **DTC and brand teams** — track your category on the fastest-growing
  marketplace, week over week.
- **Trend tools and data teams** — TikTok is where a trend starts and TikTok Shop
  is where it sells. This is the one surface where both live in the same place.

### Input

```json
{
  "searchTerms": ["funny shirt", "dolly parton"],
  "geos": ["US"],
  "maxProductsPerTerm": 20
}
```

| Field | Default | Notes |
|---|---|---|
| `searchTerms` | — | Required. Sent verbatim. Up to 100 per run. |
| `geos` | `["US"]` | US, GB, ID, MY, PH, SG, VN, TH and more. One row set per term × region. |
| `maxProductsPerTerm` | `20` | Page one is about 30 cards. Max 60. |
| `sessionCookie` | — | Optional. Only for signed-in pricing. |
| `proxyConfiguration` | **off** | Measured unnecessary — see FAQ. |

### Pricing

**$0.001 per product row** — $1.00 per 1,000 rows.

A term that came back blocked, captcha'd or empty produces no rows and **costs
nothing**. You pay for products, not for attempts.

⚠️ **Per row and not per keyword, deliberately.** The same search URL returns 5,
10 or 30 cards and the server decides — measured, fixed at page load. A
per-keyword price would charge the same for five cards as for thirty, on a coin
flip the buyer cannot see. The Actor's own cost is per keyword either way; that
variance is ours to absorb, not yours.

***

### What it does NOT do, on purpose

These are the decisions that make the numbers trustworthy. Each one costs a
feature and buys a fact.

- **It never invents a zero.** A product with no sold badge comes back
  `sold: null`, not `sold: 0`. A zero would rank a product TikTok simply did not
  count below one that genuinely sells nothing — the opposite conclusion.
- **It never writes the page size into `competingTotal`.** Thirty cards is thirty
  cards, whether thirty sellers compete or thirty thousand. Turning *"we could
  not see it"* into *"the field is empty"* is exactly the reading that gets the
  wrong product made.
- **It never returns a short list when it was blocked.** If TikTok's Security
  Check took every term, the run ends **red** with nothing pushed. A half-read
  field is indistinguishable from a thin market, and that mistake is expensive
  in one direction only.
- **It never normalizes your term.** `Dolly Parton` goes out as `Dolly Parton`.
  Lower-casing on the way in has twice cost a downstream tool hundreds of
  already-measured terms that then read as "not measured".
- **It never truncates a title.** The end of a listing title is where the detail
  that distinguishes one product from another lives — and for a print-on-demand
  brief, it *is* the evidence.
- **It never mixes regions.** Counts are reported per region and never summed,
  because marketplace counts overlap: a sum is a number that exists nowhere.
- **It never solves a captcha.** It does not need to — see below.

***

### FAQ

#### Do I need a TikTok account, login or cookies?

**No.** The Security Check guards the storefront *root*; this Actor loads the
*search page*, which is not walled the same way. Measured 5 Sep 2026, signed
out, from a fresh browser:

| page | signed out |
|---|---|
| `shop.tiktok.com/us` — the root | 🔴 Security Check, 10 KB |
| `shop.tiktok.com/us/s?q=funny shirt` — what this Actor loads | ✅ **1.1 MB, 30 real products** |

You can still pass a `sessionCookie` if you want the storefront to treat you as
a signed-in shopper (regional pricing, personalisation). It is optional.

#### Do I need a proxy?

**No, and turning one on by default was costing money for nothing.** Measured on
the Apify platform:

| run | result |
|---|---|
| 6 terms, datacenter IP, no proxy, no cookie | **6 of 6 succeeded** · 0 captchas · 50 s · 0.028 CU |
| 1 term, same | 13 s · 0.0074 CU |

RESIDENTIAL proxy is billed per GB. It is now **off** by default. Turn it on for
high volume from one address, or if reads start coming back blocked — the run
summary names every failed term and why, so you will see it happen.

#### Why is `competingTotal` always null? Can I get the number of competitors?

**TikTok Shop does not publish it, and there is no page two to count.** This was
looked for properly:

| tried | result |
|---|---|
| `&offset=30`, `&page=2`, `&cursor=30` | **ignored** — identical first product |
| scrolling the results grid | no request fires; the DOM stays put |
| the `View more` control | not reachable from the results grid |

The results component says `has_more: true` and `load_more_params.offset: 30`
and nothing more. Thirty is page one, not the field.

⚠️ The URL parameters *appeared* to work — `&page=2` returned "12 new products".
A control run of the **unchanged** URL three times returned 10, 10 and 30, which
is where those "new" products came from. The server decides how many cards to
serve, fixed at load.

**So this Actor answers "what is winning", not "how crowded is it".** If you need
a competitor count, it has to come from a marketplace that publishes one.

#### Is `sold` the product's total sales?

**No — it is a floor, and it is exact.** TikTok publishes an integer
(`sold_count: 164`) while the page renders a rounded badge. This Actor reports
the integer. It is what the storefront chose to show for that card, from page
one, so treat it as *evidence that a listing moves* — not as lifetime sales.

#### Which regions work?

`US`, `GB`, `ID`, `MY`, `PH`, `SG`, `VN`, `TH` and other TikTok Shop storefronts.
Counts are per region and never summed. Some regions return fewer results for
the same term; a region with no results produces no rows, and the run summary
names it.

#### How fast and how much?

13 seconds for one term, 50 seconds for six. About **$0.008 in compute** for a
six-term run, plus $0.001 per row.

#### What if TikTok changes its page?

The run ends **red** and the summary names which terms failed and why —
`captcha`, `no-products`, `navigation-failed`. It never finishes green with a
short list, because downstream "3 competitors" and "we were blocked" look
identical and only one of them gets a product made.

***

### Output

Two places:

- **Dataset** — one row per product, as above.
- **Key-value store `OUTPUT`** — the run summary, including **which terms
  produced nothing and why**, named rather than merely counted. A term that could
  not be read is not a term with no competition, and you need to be able to tell
  them apart.

***

*Keywords: tiktok shop scraper, tiktok shop api, tiktok shop product scraper,
tiktok shop data, scrape tiktok shop, tiktok shop sold count, tiktok shop units
sold, tiktok shop search scraper, tiktok shop no login, tiktok shop without
cookies, dropshipping product research, print on demand research, pod trend
research, tiktok shop trending products, viral products tiktok, tiktok shop
seller data, ecommerce trend research, tiktok shop scraper free.*

# Actor input Schema

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

The keywords to search on TikTok Shop. Sent EXACTLY as you type them — capitals, accents and spacing are preserved, because normalising a term is the consumer's job, not the scraper's. One result row per term and region.

## `geos` (type: `array`):

Which TikTok Shop storefronts to read, as two-letter codes (US, GB, ID…). Counts are reported PER REGION and are never summed: marketplace counts overlap, so a total across regions is a number that exists nowhere.

## `sessionCookie` (type: `string`):

⚠️ OPTIONAL — and an earlier version of this Actor wrongly required it. The Security Check guards the storefront ROOT (shop.tiktok.com/us), not the SEARCH page (shop.tiktok.com/us/s?q=…), which is the only page this Actor loads. Measured 5 Sep 2026 with no cookie at all, from a fresh browser context: the search page returned 1.1 MB and 30 real products. Every 'the cookie is required' measurement before that had been taken against the root — a page this Actor never visits. Supply one anyway if you want the storefront to treat you as a signed-in shopper (regional pricing, personalisation). Paste the whole cookie header, e.g. "sessionid=…; sessionid\_ss=…". ⚠️ Paste the NAME AND the value: DevTools shows only the Value column, and pasting that alone loses the name — the Actor says so in under a second rather than failing five retries deep. ⚠️ "sessionid" and "sessionid\_ads" are different cookies; the ads one authenticates Creative Center, not the storefront.

## `maxProductsPerTerm` (type: `integer`):

How many cards to keep from page one. Twenty is what a storefront page actually shows; more would mean paging, which multiplies exposure to the block wall for evidence nobody reads.

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

⚠️ OFF BY DEFAULT, and that is measured. On 5 Sep 2026 this Actor read 6 terms from Apify's own datacenter IP with no proxy and no session: 6 of 6 succeeded, zero captchas, 50 s, 0.028 compute units. The Security Check guards the storefront ROOT, not the search page this Actor loads — so a RESIDENTIAL proxy, which is billed per GB, was buying nothing. Turn it on if you run at high volume from one address, or if reads start coming back blocked; the run summary names every term that failed and why, so you will know.

## Actor input object example

```json
{
  "searchTerms": [
    "funny shirt",
    "dolly parton"
  ],
  "geos": [
    "US"
  ],
  "maxProductsPerTerm": 20,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `reads` (type: `string`):

One row per term and region: the competing product count and every card read from page one, with its sold badge, price, seller and image.

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

How many reads were requested, how many succeeded, and — named, not just counted — which terms produced nothing and why. A term that could not be read is not a term with no competition.

# 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": [
        "funny shirt",
        "dolly parton"
    ],
    "geos": [
        "US"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("jurassic_jove/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": [
        "funny shirt",
        "dolly parton",
    ],
    "geos": ["US"],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("jurassic_jove/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": [
    "funny shirt",
    "dolly parton"
  ],
  "geos": [
    "US"
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call jurassic_jove/tiktok-shop-scraper --silent --output-dataset

```

## MCP server setup

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