TwoSec

Scrape TikTok Shop products with Python

Search the US TikTok Shop by keyword and get every product card as JSON: title, price, discount, rating, sold count and seller. Then rank the products by sold count and save them to a CSV file. About 5 requests cover 250 products.

You can scrape the TikTok Shop storefront yourself, but then you maintain a headless browser or TikTok's internal web requests, a pool of US IP addresses and retry logic, and you fix them each time TikTok changes. This guide uses the TikTok Shop API instead: one GET request returns up to 50 products, and the JSON has the same fields every time.

Setup

You need Python 3, the requests package (pip install requests) and a key from the API's RapidAPI listing. The free plan's 100 requests are enough to run this guide. Put the key in an environment variable called RAPIDAPI_KEY.

import csv
import os
import time

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()

GET /v1/search takes a keyword in q and returns up to count product cards (maximum 50). The same request with curl:

curl --request GET \
  --url 'https://tiktok-shop-api5.p.rapidapi.com/v1/search?q=adjustable%20dumbbell&count=2' \
  --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": "1732630664915882459",
      "title": "5-in-1 Adjustable Dumbbell Set 20/40/60 lbs – Multi-Functional Home Gym Equipment for Muscle Building, Squats & Strength Training",
      "url": "https://shop.tiktok.com/us/pdp/5-in-1-adjustable-dumbbell-set-20-40-60-lbs-for-home-gym/1732630664915882459",
      "image": "https://p16-oec-general-useast5.ttcdn-us.com/…",
      "price": {
        "currency": "USD",
        "symbol": "$",
        "value": "38.38",
        "formatted": "38.38",
        "original": "53.29",
        "discount": "28%"
      },
      "rating": { "score": 4.8, "count": "9" },
      "sold": { "count": "246" },
      "seller": {
        "id": "7496210586501679579",
        "name": "Floft",
        "logo": "https://p16-oec-general-useast8.ttcdn-us.com/…"
      }
    }
  ],
  "has_more": true,
  "next_offset": 2
}

price.original and price.discount are there only when the product is on sale. sold can be missing when TikTok hides it.

Get more pages

While has_more is true, pass next_offset as the next request's offset. Cards are keyed by product ID, so a product seen on two pages is kept once.

def search(term, max_pages=5):
    cards, offset = {}, 0
    for _ in range(max_pages):
        data = get("/v1/search", q=term, count=50, offset=offset)
        for card in data.get("products") or []:
            cards[card["id"]] = card
        if not data.get("has_more"):
            break
        offset = data["next_offset"]
    return list(cards.values())


products = search("adjustable dumbbell")
print(len(products), "products")

Each page is one request, so max_pages=5 costs at most 5 requests for up to 250 products.

Rank by sold count

sold.count and rating.count are strings, formatted as TikTok shows them: "246", or "1.2K" for larger numbers. Convert them to numbers before sorting.

def to_number(text):
    """'246' -> 246, '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 sold(card):
    return to_number((card.get("sold") or {}).get("count"))


ranked = sorted(products, key=sold, reverse=True)
for card in ranked[:10]:
    print(f'{sold(card):>7}  ${card["price"]["value"]:>7}  {card["title"][:60]}')

For the card above, the printed line reads:

    246  $  38.38  5-in-1 Adjustable Dumbbell Set 20/40/60 lbs – Multi-Function

Save a CSV

Flatten each card into one row. Numbers are converted, so the CSV sorts correctly in a spreadsheet.

FIELDS = ["id", "title", "price", "original_price", "discount", "rating",
          "rating_count", "sold", "seller_id", "seller", "url"]


def row(card):
    price = card.get("price") or {}
    rating = card.get("rating") or {}
    seller = card.get("seller") or {}
    return {
        "id": card["id"],
        "title": card["title"],
        "price": price.get("value", ""),
        "original_price": price.get("original", ""),
        "discount": price.get("discount", ""),
        "rating": rating.get("score", ""),
        "rating_count": to_number(rating.get("count")),
        "sold": sold(card),
        "seller_id": seller.get("id", ""),
        "seller": seller.get("name", ""),
        "url": card["url"],
    }


with open("tiktok_shop_products.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=FIELDS)
    writer.writeheader()
    writer.writerows(row(card) for card in ranked)

The row for the card above

1732630664915882459,"5-in-1 Adjustable Dumbbell Set 20/40/60 lbs – Multi-Functional Home Gym Equipment for Muscle Building, Squats & Strength Training",38.38,53.29,28%,4.8,9,246,7496210586501679579,Floft,https://shop.tiktok.com/us/pdp/5-in-1-adjustable-dumbbell-set-20-40-60-lbs-for-home-gym/1732630664915882459

Find more keywords

GET /v1/suggest returns the autocomplete terms TikTok Shop shows for a partial query. They are phrases shoppers type, so they make good search terms for a niche.

terms = get("/v1/suggest", q="office", count=5)["suggestions"]
all_products = {}
for term in terms:
    for card in search(term, max_pages=2):
        all_products[card["id"]] = card

Real response for q=office&count=5

{
  "suggestions": [
    "office accessories",
    "office chair",
    "office outfits women",
    "office decor at work",
    "office desk chair"
  ]
}

That loop costs 1 request for the suggestions plus up to 2 per term: at most 11 requests for 5 terms.

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.