TwoSec

TikTok Shop seller products: export a store's catalog

Get every product a US TikTok Shop seller offers, with price, discount, rating and sold count, in one list. Save it each week to see what a competitor launched, discounted or started selling more of. One request covers 50 products.

Setup

You need Python 3, requests (pip install requests) and a key from the RapidAPI listing; the free plan's 100 requests are enough to try it. Put the key in an environment variable called RAPIDAPI_KEY.

import json
import os
import time
from datetime import date
from pathlib import Path

import requests

BASE = "https://tiktok-shop-api5.p.rapidapi.com"
HEADERS = {
    "X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"],
    "X-RapidAPI-Host": "tiktok-shop-api5.p.rapidapi.com",
}


def get(path, **params):
    """GET a path and return the JSON. Retries rate limits and upstream errors."""
    for attempt in range(4):
        response = requests.get(BASE + path, headers=HEADERS, params=params, timeout=30)
        if response.status_code in (429, 502, 503) and attempt < 3:
            time.sleep(2 ** attempt)
            continue
        response.raise_for_status()
        return response.json()

Find the seller ID

Every product card from product search has seller.id. If you start from a product link instead, product detail returns seller_id:

detail = get("/v1/products/1731016350288417305")
seller_id = detail["seller_id"]   # "7495957834594814489"

That product is an office chair sold by Sweetcrispy Shop. The rest of this guide uses its seller ID.

Read the catalog

GET /v1/shops/{id}/products returns the seller's products with the same card fields as search.

curl --request GET \
  --url 'https://tiktok-shop-api5.p.rapidapi.com/v1/shops/7495957834594814489/products?count=20' \
  --header "X-RapidAPI-Key: $RAPIDAPI_KEY" \
  --header 'X-RapidAPI-Host: tiktok-shop-api5.p.rapidapi.com'

Real response, shortened to one product card, image links shortened

{
  "products": [
    {
      "id": "1732242819673461273",
      "title": "Sweetcrispy 3-Wheel Foldable Compact All-Seasons Pet Stroller Carrier for Small-Med Dogs - Durable Frame, Smooth Ride, Stability, Breathable Mesh Window, Large Storage Basket (Room for Groceries), Cup Holder, Push Handle, Rear Brakes (Blue) for Outings",
      "url": "https://shop.tiktok.com/us/pdp/sweetcrispy-3-wheel-pet-stroller-with-mesh-window-foldable/1732242819673461273",
      "image": "https://p16-oec-general-useast5.ttcdn-us.com/…",
      "price": {
        "currency": "USD",
        "symbol": "$",
        "value": "70.17",
        "formatted": "70.17",
        "original": "142.97",
        "discount": "51%"
      },
      "rating": { "score": 4.6, "count": "633" },
      "sold": { "count": "4855" },
      "seller": {
        "id": "7495957834594814489",
        "name": "Sweetcrispy Shop",
        "logo": "https://p16-oec-general-useast5.ttcdn-us.com/…"
      }
    }
  ],
  "has_more": true,
  "next_offset": 20,
  "next_cursor": "40_WzE4MTEsODk5OTAwMDAsMTczNTgxMTQxNTg1OCwiMTczMDQwNTU5MjM1OTM0MjYxNyJd"
}

Get every product

Omit cursor on the first page, then pass each response's next_cursor while has_more is true. Use the cursor from your own response; the one above belongs to the example page.

def catalog(seller_id, max_pages=20):
    products, cursor = {}, None
    for _ in range(max_pages):
        params = {"count": 50}
        if cursor:
            params["cursor"] = cursor
        data = get(f"/v1/shops/{seller_id}/products", **params)
        for card in data.get("products") or []:
            products[card["id"]] = card
        cursor = data.get("next_cursor")
        if not data.get("has_more") or not cursor:
            break
    return products


products = catalog(seller_id)
print(len(products), "products")

Track changes week over week

Save each run as a dated snapshot and compare it with the previous one. This prints new and removed products, price changes and the change in sold count. The to_number helper turns TikTok's "1.2K" style counts into numbers.

def to_number(text):
    """'4855' -> 4855, '1.2K' -> 1200, None -> 0"""
    if not text:
        return 0
    text = text.strip().upper().replace(",", "")
    multiplier = {"K": 1_000, "M": 1_000_000}.get(text[-1], 1)
    if multiplier > 1:
        text = text[:-1]
    try:
        return int(float(text) * multiplier)
    except ValueError:
        return 0


def summary(card):
    return {
        "title": card["title"][:80],
        "price": (card.get("price") or {}).get("value"),
        "sold": to_number((card.get("sold") or {}).get("count")),
    }


folder = Path("snapshots") / seller_id
folder.mkdir(parents=True, exist_ok=True)
current = {pid: summary(card) for pid, card in products.items()}
previous_files = sorted(folder.glob("*.json"))
previous = json.loads(previous_files[-1].read_text()) if previous_files else None
(folder / f"{date.today()}.json").write_text(json.dumps(current))

if previous is not None:
    for pid in current.keys() - previous.keys():
        print("NEW     ", current[pid]["price"], current[pid]["title"])
    for pid in previous.keys() - current.keys():
        print("REMOVED ", previous[pid]["title"])
    for pid in current.keys() & previous.keys():
        old, new = previous[pid], current[pid]
        if old["price"] != new["price"]:
            print("PRICE   ", old["price"], "->", new["price"], new["title"])
        if new["sold"] > old["sold"]:
            print("SOLD +", new["sold"] - old["sold"], new["title"])

Run it once a week (a cron job is enough). For the pet stroller above, a PRICE line would show 70.17 and the new price.

Things to know

Next steps

Plans

A free plan covers testing. Plans, monthly quotas and rate limits are listed on the RapidAPI listing. For more volume or a custom plan, use Contact provider on the listing.