# TikTok Shop API — product detail with per-SKU price and stock, category browsing, seller profiles and catalogues, US keyword search, best-seller and deal feeds, and customer reviews across 10 markets (US, UK, Malaysia, Singapore, Thailand, Philippines, Vietnam, Indonesia, Japan, Mexico)

> Full TikTok Shop product record in ONE call, for any of the ten markets — title, structured description, every image, EVERY SKU with its own price and its exact remaining stock as an integer, shipping weight in grams and package size in centimetres, discount and list price, shipping fee and delivery window, category path, the seller's full profile, three customer reviews and the complete star histogram. Nothing here needs a second request
> ReefAPI engine `tiktok-shop` · 13 endpoints · clean JSON, no scraping or browsers to manage.

## How to call
- **Endpoint:** `POST https://api.reefapi.com/tiktok-shop/v1/<action>` with a JSON body.
- **Auth:** header `x-api-key: <YOUR_REEFAPI_KEY>` — create one free (1,000 credits, no card): https://reefapi.com/signup
- **Response (every call):** `{ ok: boolean, data: ..., meta: { record_count, credits, ... }, error: { code, message } }` — branch on `ok`. Failed or blocked calls are free.
- **One key + one shared credit pool** across every ReefAPI API. Per-call credits are listed on each endpoint below.
- **Use it from an AI agent (MCP):** connect `https://api.reefapi.com/mcp` (remote streamable-http, `Authorization: Bearer <key>`) and your assistant can call these actions directly.

## Endpoints

### POST /tiktok-shop/v1/product_detail — 2 credits
Full TikTok Shop product record in ONE call, for any of the ten markets — title, structured description, every image, EVERY SKU with its own price and its exact remaining stock as an integer, shipping weight in grams and package size in centimetres, discount and list price, shipping fee and delivery window, category path, the seller's full profile, three customer reviews and the complete star histogram. Nothing here needs a second request

**Parameters:**
- `product_id` (string, required) — TikTok Shop product id — the 19-digit number at the end of a product URL (shop.tiktok.com/us/pdp/product/1729450858130215273). It is a stable string, returned verbatim by every action, and it never changes type or gets re-keyed between surfaces.
- `market` (enum, optional, default "us") — Which TikTok Shop market to read. Ten are supported and all ten are live-verified with the correct local currency: us (USD), gb (GBP), my (MYR), sg (SGD), th (THB), ph (PHP), vn (VND), id (IDR), jp (JPY), mx (MXN). The market decides the storefront, the currency and the catalogue — the same product id can be absent from another market. Brazil is listed by TikTok but is not reachable and is not offered; every European market other than the UK is a seller-side region, not a shop. [one of: us, gb, my, sg, th, ph, vn, id, jp, mx]

**Returns:** product{product_id, title, url, market, currency, price, list_price, discount_percent, is_promotional, rating, review_count, sold_count, brand, images[], description, description_images[], categories[], specifications[], skus[], total_stock, sku_count, shipping{}, seller{}}, reviews[], review_summary{}. PRICE IS NOT A CONSTANT — read is_promotional first. On a product that is not on promotion the price was identical across 8 consecutive fetches; on a PROMOTED product TikTok re-throws both the sale price and the 'was' price on every request (the discount ratio stays fixed), and the payload contains no un-promoted base price at all. So list_price and discount_percent are present when and only when is_promotional is true, and both are marketing numbers, not a price history. SKUS: skus[] carries sku_id, sku_name, properties[] (colour/size/…), available_quantity as a real integer, in_stock, per-SKU price, image, and package_weight_g / package_length_cm / package_width_cm / package_height_cm. Those package_* fields are ALWAYS grams and centimetres. TikTok also publishes weight/dimension objects with a per-product unit flag that flips between metric and imperial — this API does not return them, because a parser that trusts them reports '1 gram' for a pair of headphones. Note the package weight is the SHIPPING weight; a product weight quoted in specifications[] is a different number and the two are not merged. RATING: null when the product has no reviews yet — TikTok emits a literal 0 there, which would read as 'scored zero out of five'. review_count is 0 in that case, not null, so you can tell 'unrated' from 'unknown'. REVIEWS: exactly 3 are embedded (the site itself embeds no more), plus review_summary with total_reviews, average_rating and the full 1-5 star rating_distribution — so the histogram covers all reviews even though the text sample is three. NOT-FOUND IS CONFIRMED, NOT GUESSED: TikTok returns the same body for 'no such product' and 'this exit was refused', so a single negative is never reported as NOT_FOUND — it is re-checked against a second, independent route or exit first, and an unconfirmed negative comes back as a retryable TARGET_BLOCKED instead.

**Example request body:**
```json
{
  "product_id": "1729450858130215273",
  "market": "us"
}
```

### POST /tiktok-shop/v1/products_by_ids — 1 credit
Look up as many as 15 US products in a SINGLE call — the cheapest way to refresh a list of ids you already hold, at roughly a sixth of the bandwidth per product of fetching each product page. It is also the only route on TikTok Shop with an unambiguous 'not found', which makes it the right way to check whether an id is still live before spending a full product fetch on it. US catalogue only

**Parameters:**
- `product_ids` (array, required) — Up to 15 TikTok Shop product ids. Accepts a JSON array or a comma-separated string. More than 15 ids are split into several upstream calls automatically.

**Returns:** products[] (the standard listing row: product_id, title, url, slug, image, brand, seller_id, seller_name, rating, review_count, sold_count, sku_count, labels[], price, list_price, currency, discount_percent, is_promotional), not_found[], requested, returned. not_found[] is TRUSTWORTHY on this action and on no other: a bogus id makes this route answer with an empty product list in 56 bytes, which no other TikTok Shop route does. An id in not_found[] is genuinely absent from the US catalogue. WHAT THIS IS NOT: a product_detail substitute. It returns the listing card, so there are NO skus[], no per-SKU stock, no package weight or dimensions, no description and no reviews. Use it to price and validate a list; use product_detail when you need the record. US market only — the route answers code 10000 on all nine other market edges and 404 on the UK edge.

**Example request body:**
```json
{
  "product_ids": [
    "1729450858130215273",
    "1729397197029216665"
  ]
}
```

### POST /tiktok-shop/v1/category_tree — 1 credit
TikTok Shop's own category taxonomy for any of the ten markets — 28 top-level departments from one call, then walk down to level 2 and level 3 by passing a category_id. Every node says whether it is a leaf, so the tree can be crawled to the bottom with no external id list and nothing hard-coded

**Parameters:**
- `market` (enum, optional, default "us") — Which TikTok Shop market to read. Ten are supported and all ten are live-verified with the correct local currency: us (USD), gb (GBP), my (MYR), sg (SGD), th (THB), ph (PHP), vn (VND), id (IDR), jp (JPY), mx (MXN). The market decides the storefront, the currency and the catalogue — the same product id can be absent from another market. Brazil is listed by TikTok but is not reachable and is not offered; every European market other than the UK is a seller-side region, not a shop. [one of: us, gb, my, sg, th, ph, vn, id, jp, mx]
- `category_id` (string, optional) — Expand this category's children instead of the root. Omit for the 28 top-level departments. Ids come from this action, or from any product's categories[].
- `slug` (string, optional) — URL slug of the category, as returned in slug by this action. Optional — a placeholder is used when omitted, since TikTok routes on the numeric id.

**Returns:** categories[] {category_id, name, name_en, slug, level, is_leaf, parent_category_id, image, url}, parent{}, breadcrumbs[], market, level. is_leaf true means the node has no children and is where products live — feed that category_id straight into category_products. The taxonomy is three levels deep (28 departments → ~7-10 children each → leaves) and it is the SAME id space the product action returns in categories[], so a product can be mapped onto the tree without a lookup table. name is TikTok's localised label for the market; name_en is the stable English slug-form, which is what you should key on.

**Example request body:**
```json
{
  "market": "us"
}
```

### POST /tiktok-shop/v1/category_products — 1 credit
Browse a whole category and page through it — around 100 products per category, on ALL TEN markets. This is the cheapest source of product ids there is, roughly 1 KB of traffic per product, and it is what replaces keyword search outside the US. Pages are position-independent, so a page that gets throttled is retried automatically without losing your place

**Parameters:**
- `category_id` (string, required) — Numeric category id from the category_tree action or from a product's categories[]. Leaf categories give the most focused list, but any level works.
- `market` (enum, optional, default "us") — Which TikTok Shop market to read. Ten are supported and all ten are live-verified with the correct local currency: us (USD), gb (GBP), my (MYR), sg (SGD), th (THB), ph (PHP), vn (VND), id (IDR), jp (JPY), mx (MXN). The market decides the storefront, the currency and the catalogue — the same product id can be absent from another market. Brazil is listed by TikTok but is not reachable and is not offered; every European market other than the UK is a seller-side region, not a shop. [one of: us, gb, my, sg, th, ph, vn, id, jp, mx]
- `max_results` (integer, optional, default 50) — How many products to return, 1-200 (default 50). Values above the maximum are clamped, not rejected, and a value below one page still costs one upstream call. Fewer results really is cheaper here — the page size is passed through to TikTok, so asking for 5 transfers roughly a third of what 20 does.
- `slug` (string, optional) — Category URL slug from category_tree. Optional; it only makes the referring page URL exact.

**Returns:** results[] (standard listing rows), category_id, market, has_more, pages_fetched, requested_max. DEPTH IS THE SOURCE'S, NOT OURS. TikTok's own UI caps a category at 100 products and the measured ceiling is 82-115 depending on the category and the run — the walk ends on TikTok's own has_more:false, not on a limit of ours, so asking for 200 gets you everything that exists rather than an error. A few categories run out earlier because they genuinely hold fewer products; meta.stop_reason says which ending you got. Rows carry seller_id and seller_name, so a category page doubles as a free seller-discovery feed (20 distinct shops per page) that feeds seller_profile and seller_catalog. ALL TEN MARKETS. The US edge needs its session warmed before it will answer, which this API does for you; the other nine answer directly. Deduplication is exact — the same product never appears on two pages.

**Example request body:**
```json
{
  "category_id": "601262",
  "market": "us",
  "max_results": 20
}
```

### POST /tiktok-shop/v1/search — 1 credit
Keyword search across the US TikTok Shop catalogue, paged for you up to 150 products, with price, rating, sold count and seller on every row. US ONLY — TikTok does not serve keyword search on any other market, at any URL (use category_products there, which goes deeper than search does)

**Parameters:**
- `query` (string, required) — Search keyword, exactly as a shopper would type it.
- `max_results` (integer, optional, default 30) — How many products to return, 1-150 (default 30). Values above the maximum are clamped, not rejected, and a value below one page still costs one upstream call. Fewer results really is cheaper here — the page size is passed through to TikTok, so asking for 5 transfers roughly a third of what 20 does.

**Returns:** results[] (standard listing rows), shops[] {seller_id, shop_name, shop_logo}, query, has_more, pages_fetched, requested_max. NO SORT AND NO FILTER PARAMETERS, DELIBERATELY. TikTok's search response advertises its own sort and filter descriptors, and every one of them was sent back exactly the way its own interface sends them: 'price low to high' returned an unsorted list, and 'max price $30' returned the identical thirty products with nine of them over $30. The server accepts the field, answers success and ignores it. Shipping a knob that does nothing is worse than not shipping it, so sort the results yourself. DEPTH: about 150 products per keyword is where TikTok stops (has_more goes false); some keywords stop earlier. QUALITY GUARD: TikTok intermittently answers a search with a frozen 10-item generic feed that carries a success code and looks like a result — the same ten products every time, unrelated to the query. This API detects that feed and retries on a fresh connection instead of returning it, so you never receive one. shops[] is the 'shops matching your search' rail and comes free with the page.

**Example request body:**
```json
{
  "query": "yoga mat",
  "max_results": 30
}
```

### POST /tiktok-shop/v1/recommended_shops — 1 credit
Shops TikTok itself surfaces for a search term — a keyword-to-seller lookup that answers 'who sells this' rather than 'what is for sale'. Comes free alongside the US search page, so it costs one call and no extra bandwidth. US only

**Parameters:**
- `query` (string, required) — Search keyword to find shops for.

**Returns:** shops[] {seller_id, shop_name, shop_logo, verified}, query, count. seller_id feeds straight into seller_profile and seller_catalog. DEPTH IS SMALL AND HONEST: TikTok attaches between 0 and about 5 shops to a search page, and for some keywords it attaches none — an empty shops[] with ok:true means TikTok surfaced no shop rail for that term, not that the call failed. This is a rail, not a shop directory; there is no pagination behind it. US only, for the same reason as search.

**Example request body:**
```json
{
  "query": "yoga mat"
}
```

### POST /tiktok-shop/v1/seller_profile — 1 credit
A TikTok Shop seller's full public profile on any of the ten markets — shop name, rating, review count, follower count, lifetime units sold, catalogue size, video count and TikTok's own service sub-scores. On the nine non-US markets there is no seller endpoint at all, so this is reached through one of the seller's products; pass a product_id and you get the seller for free

**Parameters:**
- `seller_id` (string, optional) — TikTok Shop seller id. Every product record carries it under seller_id, so the usual flow is search/category → product → seller_id → this action.
- `product_id` (string, optional) — TikTok Shop product id — the 19-digit number at the end of a product URL (shop.tiktok.com/us/pdp/product/1729450858130215273). It is a stable string, returned verbatim by every action, and it never changes type or gets re-keyed between surfaces.
- `market` (enum, optional, default "us") — Which TikTok Shop market to read. Ten are supported and all ten are live-verified with the correct local currency: us (USD), gb (GBP), my (MYR), sg (SGD), th (THB), ph (PHP), vn (VND), id (IDR), jp (JPY), mx (MXN). The market decides the storefront, the currency and the catalogue — the same product id can be absent from another market. Brazil is listed by TikTok but is not reachable and is not offered; every European market other than the UK is a seller-side region, not a shop. [one of: us, gb, my, sg, th, ph, vn, id, jp, mx]

**Returns:** seller{seller_id, global_seller_id, shop_name, creator_name, description, region, shop_url, shop_logo, shop_rating, best_rating, worst_rating, review_count, sold_count, global_sold_count, followers_count, video_count, on_sell_product_count, sub_scores[]}, source, market. Pass seller_id (US only — the dedicated seller endpoint exists on the US edge and returns 404 on the other nine) OR product_id (all ten markets — the profile is embedded in every product page). Give product_id and this action costs nothing beyond the product fetch you were going to make anyway; source tells you which path was used. TWO SHAPES, BOTH REAL: most shops publish 26 fields, some publish a 20-field display mode that omits followers_count, video_count, creator_name and enable_follow. Missing values are returned as null and are never substituted — in particular a null shop_rating (seen on Mexican shops that still publish a review_count) is left null rather than filled in from best_rating. sub_scores[] is TikTok's own service scorecard (ships-on-time, replies-in-time…) as {type, score_percent}. shop_url is the store link TikTok publishes itself; be aware that page currently 404s on every market, so treat it as an identifier rather than a working link.

### POST /tiktok-shop/v1/seller_catalog — 1 credit
Every product a seller has listed, paged to the end — verified complete against the shop's own advertised product count (12 of 12, 81 of 81, 28 of 28 on three separate markets). Nine markets: US, Malaysia, Singapore, Thailand, Philippines, Vietnam, Indonesia, Japan and Mexico. NOT the UK, where TikTok does not deploy the seller catalogue service at all

**Parameters:**
- `seller_id` (string, required) — TikTok Shop seller id. Every product record carries it under seller_id, so the usual flow is search/category → product → seller_id → this action.
- `market` (enum, optional, default "us") — Which TikTok Shop market to read. Ten are supported and all ten are live-verified with the correct local currency: us (USD), gb (GBP), my (MYR), sg (SGD), th (THB), ph (PHP), vn (VND), id (IDR), jp (JPY), mx (MXN). The market decides the storefront, the currency and the catalogue — the same product id can be absent from another market. Brazil is listed by TikTok but is not reachable and is not offered; every European market other than the UK is a seller-side region, not a shop. [one of: us, gb, my, sg, th, ph, vn, id, jp, mx]
- `max_results` (integer, optional, default 60) — How many products to return, 1-240 (default 60). Values above the maximum are clamped, not rejected, and a value below one page still costs one upstream call. Fewer results really is cheaper here — the page size is passed through to TikTok, so asking for 5 transfers roughly a third of what 20 does.

**Returns:** results[] (standard listing rows, plus brand where the seller set one), seller_id, market, has_more, pages_fetched, requested_max. COMPLETENESS IS SELF-VERIFYING: cross-check the row count against the on_sell_product_count returned by seller_profile. Small and mid-size shops were walked to exactly that number and then has_more went false. Large shops (2,000+ listings) were still returning has_more:true at 240 products with no ceiling found, so for those, max_results is your limit rather than the source's. UK IS NOT SUPPORTED and asking for it returns a clear MARKET_UNAVAILABLE rather than an empty list — the whole service this route belongs to is missing from the UK edge. UK seller PROFILES do work; use seller_profile with a product_id. THROTTLING IS HANDLED, NOT HIDDEN: this route intermittently refuses a request and answers on retry seconds later, so pages are retried across alternate hosts automatically. On some markets a refusal and 'no such seller' are byte-identical, so a seller id that never answers comes back as a retryable error, not as an empty catalogue that would read like a real answer.

### POST /tiktok-shop/v1/related_products — 2 credits
The products TikTok itself recommends alongside a given product — its 'you may also like' rail, about 25 full product rows. Useful for competitor sets, substitutes and building a similar-items shelf. Works on all ten markets

**Parameters:**
- `product_id` (string, required) — TikTok Shop product id — the 19-digit number at the end of a product URL (shop.tiktok.com/us/pdp/product/1729450858130215273). It is a stable string, returned verbatim by every action, and it never changes type or gets re-keyed between surfaces.
- `market` (enum, optional, default "us") — Which TikTok Shop market to read. Ten are supported and all ten are live-verified with the correct local currency: us (USD), gb (GBP), my (MYR), sg (SGD), th (THB), ph (PHP), vn (VND), id (IDR), jp (JPY), mx (MXN). The market decides the storefront, the currency and the catalogue — the same product id can be absent from another market. Brazil is listed by TikTok but is not reachable and is not offered; every European market other than the UK is a seller-side region, not a shop. [one of: us, gb, my, sg, th, ph, vn, id, jp, mx]
- `max_results` (integer, optional, default 25) — How many related products to return, 1-60 (default 25). Values above the maximum are clamped, not rejected, and a value below one page still costs one upstream call. Fewer results really is cheaper here — the page size is passed through to TikTok, so asking for 5 transfers roughly a third of what 20 does.

**Returns:** results[] (standard listing rows), product_id, market, source, count. These are TikTok's own recommendations for the product page, captured from the page's own recommendation component and expanded through TikTok's mobile service — not a re-sorted search and not a category slice. source names which rail answered. TikTok publishes two rails on a product page: 'you may also like' (recommendations, the one that returns data) and 'more from this shop' (the seller's other listings). The second rail's service currently returns an empty list for every request shape tried, so it is not offered as a promise here — for a seller's other products use seller_catalog, which is complete. Depth is what the rail holds, around 25; there is no deep pagination behind it.

**Example request body:**
```json
{
  "product_id": "1729450858130215273"
}
```

### POST /tiktok-shop/v1/ranking_list — 2 credits
TikTok Shop's own best-seller board for the US — the 20 products TikTok is promoting as trending right now, in their published rank order, with full price, rating and seller rows. This is the editorial ranking the storefront shows, not a re-sorted search, which is what makes it the winning-products feed

**Parameters:** none

**Returns:** results[] (standard listing rows, in TikTok's own rank order — index 0 is rank 1), count, rank_field. DEPTH IS EXACTLY WHAT THE BOARD HOLDS: 19-20 products. There is no pagination behind it — the paging service does not serve this component type, which was tested rather than assumed — so 20 is the whole board, not a page of it. US only. Refresh it daily; the board turns over, and cache_ttl reflects that.

### POST /tiktok-shop/v1/premium_offers — 1 credit
TikTok Shop's US 'premium offers' deal board, paged to the end — around 97 discounted products with the discount percentage and the pre-discount price on every row. A ready-made deals feed rather than a price filter over search

**Parameters:**
- `max_results` (integer, optional, default 80) — How many deal products to return, 1-240 (default 80). Values above the maximum are clamped, not rejected, and a value below one page still costs one upstream call. Fewer results really is cheaper here — the page size is passed through to TikTok, so asking for 5 transfers roughly a third of what 20 does.

**Returns:** results[] (standard listing rows; is_promotional is true and list_price and discount_percent are populated on essentially every row, which is the point of this board), has_more, pages_fetched, requested_max. The board holds about 97 products in total and ends on TikTok's own has_more going false, so asking for more than that returns everything there is. Pagination replays the deal component the live page itself ships — guessed request shapes make this route answer 'success' with an empty list, so this action always reads the real board first. US only.

### POST /tiktok-shop/v1/recommended_for_you — 1 credit
TikTok Shop's US personalised discovery feed, paged and de-duplicated — 200-plus distinct products of what TikTok is currently pushing to shoppers. The widest trend-discovery surface this API has

**Parameters:**
- `max_results` (integer, optional, default 80) — How many products to return, 1-240 (default 80). Values above the maximum are clamped, not rejected, and a value below one page still costs one upstream call. Fewer results really is cheaper here — the page size is passed through to TikTok, so asking for 5 transfers roughly a third of what 20 does.

**Returns:** results[] (standard listing rows), has_more, pages_fetched, duplicates_dropped, requested_max. THE FEED REPEATS ITSELF AND THIS API DE-DUPLICATES IT. TikTok keeps answering has_more:true while the share of genuinely new products decays page by page (40, 40, 40, 39, 27, 16, 11 new items over seven pages in the recon run). Rows are de-duplicated by product_id, duplicates_dropped tells you how much repetition there was, and the walk stops early when a page brings nothing new — so max_results is a ceiling, not a promise, and meta.stop_reason says which ending you got. Because it is a recommendation feed it is NOT stable between calls; do not treat it as a catalogue. US only.

### POST /tiktok-shop/v1/reviews — 1 credit
Customer reviews for a product on any of the ten markets — three full review texts with rating, author, verified-purchase flag and photo, plus the COMPLETE 1-to-5 star histogram and total review count covering every review the product has. Served from a 7 KB endpoint instead of the product page, so a ratings refresh costs a fraction of a re-fetch

**Parameters:**
- `product_id` (string, required) — TikTok Shop product id — the 19-digit number at the end of a product URL (shop.tiktok.com/us/pdp/product/1729450858130215273). It is a stable string, returned verbatim by every action, and it never changes type or gets re-keyed between surfaces.
- `market` (enum, optional, default "us") — Which TikTok Shop market to read. Ten are supported and all ten are live-verified with the correct local currency: us (USD), gb (GBP), my (MYR), sg (SGD), th (THB), ph (PHP), vn (VND), id (IDR), jp (JPY), mx (MXN). The market decides the storefront, the currency and the catalogue — the same product id can be absent from another market. Brazil is listed by TikTok but is not reachable and is not offered; every European market other than the UK is a seller-side region, not a shop. [one of: us, gb, my, sg, th, ph, vn, id, jp, mx]

**Returns:** reviews[] {review_id, rating, text, author, author_avatar, sku_id, review_time_ms, verified_purchase, incentivized, image}, review_summary{total_reviews, average_rating, rating_distribution{1..5}}, product_id, market, source. READ THIS BEFORE BUYING: the review TEXT depth is THREE. That is all TikTok serves publicly — the paged review service exists and its full parameter model is known (page size, sort, star/media/verified filters), but it rejects every unauthenticated request, including a genuine Chrome call made from inside the live page, which is why this is described as a login gate rather than a limitation of this API. We do not run TikTok accounts, so we do not sell review depth we cannot deliver. Competitors who advertise hundreds of reviews per product are doing something we do not do. WHAT IS COMPLETE: review_summary. total_reviews and the five-bucket rating_distribution cover ALL reviews (they sum to total_reviews, verified on products with 6,000 and 40,000 reviews), so rating analytics are fully served even though the text sample is three. The cheap review endpoint is intermittent — it refuses more often than it answers on some markets — so this action falls back to reading the same three reviews and the same histogram out of the product page. source tells you which one answered, and the fallback costs more bandwidth but returns identical data.

**Example request body:**
```json
{
  "product_id": "1729450858130215273"
}
```

## More
- Try it live, no code: https://reefapi.com/playground?engine=tiktok-shop
- Human docs page: https://reefapi.com/docs/tiktok-shop
- Every ReefAPI API in one file (for your AI): https://reefapi.com/llms-full.txt
