TwoSec

TikTok Shop reviews scraper: export reviews with Python

Pull the reviews of any US TikTok Shop product into a CSV file: star rating, review text, the SKU the buyer chose, country, and verified-purchase and incentivized flags. You also get the product's 1★–5★ breakdown. A product with 400 reviews takes 9 requests.

TikTok Shop loads reviews after the page renders, so a plain HTTP scraper doesn't get them and a browser scraper has to click through pages. The TikTok Shop API returns up to 50 reviews per request as JSON.

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 csv
import os
import re
import time
from collections import Counter

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 the product ID

The product ID is the long number at the end of a TikTok Shop product link, such as https://shop.tiktok.com/us/pdp/1731016350288417305. You can also take id from a product search result.

def product_id(link_or_id):
    match = re.search(r"(\d{15,20})/?(?:\?|$)", link_or_id.strip())
    if not match:
        raise ValueError(f"no product ID in {link_or_id!r}")
    return match.group(1)


pid = product_id("https://shop.tiktok.com/us/pdp/1731016350288417305")

Read one page

GET /v1/products/{id}/reviews takes page (from 1) and page_size (up to 50).

curl --request GET \
  --url 'https://tiktok-shop-api5.p.rapidapi.com/v1/products/1731016350288417305/reviews?page=1&page_size=20' \
  --header "X-RapidAPI-Key: $RAPIDAPI_KEY" \
  --header 'X-RapidAPI-Host: tiktok-shop-api5.p.rapidapi.com'

Real response for an office chair, shortened to one review

{
  "has_more": true,
  "total": "409",
  "summary": {
    "count": "409",
    "score": 4.6,
    "breakdown": { "1": 22, "2": 6, "3": 12, "4": 33, "5": 336 }
  },
  "reviews": [
    {
      "id": "7685937594455688974",
      "rating": 5,
      "text": "This little chair is not only cute,  I chose the blue color,  but also very comfortable.",
      "author": "c**7",
      "time": "2026-09-16T01:23:26Z",
      "sku": "Blue",
      "country": "US",
      "verified": true,
      "incentivized": false
    }
  ]
}

rating is an integer from 1 to 5, time is UTC, and author comes masked. A product with no reviews returns total: "0", an empty breakdown and reviews: null.

Get every review

Increase page while has_more is true. Stop on has_more, not on total: total can be a few higher than the rows returned.

def all_reviews(pid):
    reviews, summary, page = [], None, 1
    while True:
        data = get(f"/v1/products/{pid}/reviews", page=page, page_size=50)
        summary = summary or data.get("summary") or {}
        reviews.extend(data.get("reviews") or [])
        if not data.get("has_more"):
            return summary, reviews
        page += 1


summary, reviews = all_reviews(pid)
breakdown = summary.get("breakdown") or {}
print(f'{summary.get("count", "0")} reviews, average {summary.get("score", "-")}')
print("  ".join(f"{star}★ {breakdown.get(star, 0)}" for star in "54321"))

Output for the office chair

409 reviews, average 4.6
5★ 336  4★ 33  3★ 12  2★ 6  1★ 22

Save a CSV

FIELDS = ["id", "rating", "time", "sku", "country", "verified", "incentivized", "text"]

with open(f"reviews_{pid}.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=FIELDS, extrasaction="ignore")
    writer.writeheader()
    writer.writerows(reviews)
print(f"saved {len(reviews)} reviews")

The row for the review shown above

7685937594455688974,5,2026-09-16T01:23:26Z,Blue,US,True,False,"This little chair is not only cute,  I chose the blue color,  but also very comfortable."

Find the complaints

Three questions most sellers and product researchers ask of a review export:

# 1. Which variant gets the bad reviews?
low = [r for r in reviews if r["rating"] <= 2]
print(Counter(r.get("sku") or "-" for r in low).most_common(5))

# 2. What is the average without incentivized reviews?
organic = [r["rating"] for r in reviews if not r.get("incentivized")]
if organic:
    print("organic average:", round(sum(organic) / len(organic), 2))

# 3. What do unhappy verified buyers say?
for r in low:
    if r.get("verified"):
        print(r["rating"], r.get("sku"), (r.get("text") or "")[:120])

To compare competing products, run all_reviews on each product ID from a search and compare their breakdowns and low-star SKUs side by side.

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.