# TikTok Shop API: 52 products over 3 credits (https://www.socialcrawl.dev/blog/tiktok-shop-api-catalogues-reviews)
> One TikTok Shop API walk returned 52 of 52 products over 3 credits, plus 60 of 1,125 reviews. Same product object from search and shop. Live 2026-09-08.
On 8 September 2026, a TikTok Shop API walk of the Goli Nutrition storefront returned **52 of 52 products over 3 pages for 3 credits** (20 + 20 + 12). The same harvest pulled **60 distinct reviews over 6 pages for 6 credits** from a Potaroma cat-nail-clipper listing, out of **1,125** reported on page 1. `has_more` was still true. Sixteen production calls billed **13 credits**.
Google's top results for that query are official — and the wrong API. [Research Query TikTok Shop Info](https://developers.tiktok.com/docs/en/research-api-specs-query-tiktok-shop-info) is EU shop *metadata*, max **10**, scope `research.data.basic` — not a catalogue. The [Partner / TTS API](https://partner.tiktokshop.com/docv2/page/tts-api-concepts-overview) is seller-gated OAuth for **authorized** shops — the catalogues you already own. This post is the measured public catalogue and reviews walk on the SocialCrawl API. For Partner versus public, see [which TikTok Shop API](/blog/best-tiktok-shop-apis-2026).
**Stack:** Python 3.10+ · `requests` · `GET https://www.socialcrawl.dev/v1/tiktokshop/…` · header `x-api-key`. Print `len(items)` and `credits_used`. Pagination is one billed page per call. Free plan: [100 credits, no card](/pricing). Live catalogue: [`/platforms/tiktokshop`](/platforms/tiktokshop).
Goli Nutrition, SocialCrawl GET, 8 September 2026: **52 of 52** products
over **3 credits** (20 + 20 + 12). Reviews: **60** distinct of **1,125**
reported over **6 credits**, not exhausted. Same product object from search
and shop listing. Seller description on detail: **1,288** characters. One
US shop, one review product.
## Does a TikTok Shop API page a whole shop catalogue?
Call `GET /v1/tiktokshop/products` with the shop URL and keep passing `pagination.next_cursor` as `cursor` until `has_more` is false. Page size is not a caller `limit`. Print `len(items)` and `credits_used` on every page.
Live on the documented Goli Nutrition storefront (`https://www.tiktok.com/shop/store/goli-nutrition/7495794203056835079`), 2026-09-08, 00:17–00:21 UTC:
| Page | Items | `data.total` | `dropped` | `has_more` | Credits | `request_id` |
| ---: | ----: | -----------: | --------: | ---------- | ------: | ------------ |
| 1 | **20** | 52 | 0 | true | 1 | `req-icWAu9rbauLx3sRS` |
| 2 | **20** | 52 | 0 | true | 1 | `req-kj0NIF4mF9guocdL` |
| 3 | **12** | 52 | 0 | **false** | 1 | `req-gqF6spgOxKoRGtpH` |
| **Walk** | **52 unique / 0 dupes** | 52 | 0 | stopped | **3** | |
Seller on every row: `"Goli Nutrition"`. Page size is 20 until the last page (12). Shop `data.total` was stable at **52**.
Labeled before (2026-09-07 ship note, not re-measured today): this call used to stop at 20 and report that as the whole shop. Live it pages.
A first-page-only read is incomplete after even a few days of official TTS quota. New shops sit at a [100 product uploads per day](https://partner.tiktokshop.com/docv2/page/products-api-overview) cap; after graduation it is 1,000. Commerce lists are walks: Shopify REST iterates [`rel=next` until it disappears](https://shopify.dev/docs/api/admin-rest/usage/pagination). This fixture is the documented example storefront, not a random sample. Do not generalise "every shop has 52 products".
List rows do **not** carry the long description, stock, shipping, or SKU list. On the first listing row, `description`, `brand`, and `availability` were **null** — those fields live on product detail.
Worked row (page 1, first product): id `1729527313880355335`, seller `Goli Nutrition`, price `{current: 14.98, original: 19, currency: "USD"}`, rating 4.5 / 94,634, `ext.sold_count` 1,301,493, `shop_products_count` 52, `shop_sold_count` 5,726,901.
```bash
curl "https://www.socialcrawl.dev/v1/tiktokshop/products?url=https://www.tiktok.com/shop/store/goli-nutrition/7495794203056835079" \
-H "x-api-key: $SOCIALCRAWL_API_KEY"
```
Trimmed first item (`request_id` `req-icWAu9rbauLx3sRS`). Envelope: 20 items, `credits_used=1`, `has_more=true`.
```json
{
"product": {
"id": "1729527313880355335",
"url": "https://www.tiktok.com/shop/pdp/1729527313880355335",
"title": "Goli Ashwagandha & Vitamin D Gummy - Mixed Berry, KSM-66, Vegan, Plant Based, Non-GMO, Gluten-Free & Gelatin Free. America's #1 Ashwagandha Brand",
"description": null,
"seller": "Goli Nutrition",
"brand": null,
"price": { "current": 14.98, "original": 19, "currency": "USD" },
"rating": { "average": 4.5, "count": 94634 },
"availability": null,
"reviews_count": 94634,
"ext": {
"sold_count": 1301493,
"tiktokshop": {
"shop_name": "Goli Nutrition",
"shop_rating": 4.6,
"shop_products_count": 52,
"shop_sold_count": 5726901,
"shop_region": "US"
}
}
}
}
```
The Python cursor loop lives in the paging section below.
## How many reviews does a TikTok Shop reviews API return per credit?
**10** reviews a page, 1 credit. That is a walk, not a 10-row window presented as the corpus.
`GET /v1/tiktokshop/product/reviews` on `https://www.tiktok.com/shop/pdp/cat-nail-clipper-by-potaroma-adjustable-sizes-built-in-file-safe-for-kittens-cats/1731578642912612516`, walked `pagination.next_cursor` as `cursor`, stopped after 6 pages on purpose:
| Page | Items | Distinct | `data.total` | `has_more` | Credits | `request_id` |
| ---: | ----: | -------: | -----------: | ---------- | ------: | ------------ |
| 1 | 10 | 10 | **1125** | true | 1 | `req-mgfNFIiO6IXbbMmx` |
| 2 | 10 | 20 | 1120 | true | 1 | `req-zeo0Q70FPbXMpQ2R` |
| 3 | 10 | 30 | **924** | true | 1 | `req-4KcmN8zLWhtWN8rc` |
| 4 | 10 | 40 | 1124 | true | 1 | `req-X0b6p9VGinYHaKKc` |
| 5 | 10 | 50 | 1125 | true | 1 | `req-UXNB7lOdfdeedfWl` |
| 6 | 10 | **60** | 1125 | **true** (stopped) | 1 | `req-FO6sRQUwCzBvD9i2` |
| **Walk** | **60** | **60 unique, 0 dupes** | use **1,125** | not exhausted | **6** | |
That is **60 distinct reviews over 6 pages, 6 credits, out of 1,125 reported.** The walk did not finish the corpus — `has_more` was still true on page 6.
`data.total` moved between pages: 1125, 1120, **924**, 1124, 1125, 1125. Use **1,125** from page 1 as the reported corpus. The live count is not a frozen integer. Shop `data.total` was stable at 52; reviews were not.
Labeled before (2026-09-07 note): this path used to stop at 10, no paging. Official Research [Query TikTok Shop Reviews](https://developers.tiktok.com/docs/en/research-api-specs-query-tiktok-shop-reviews) pages at `page_size` max **10**, EU-only, rating enum `"FIVE"`. That is not a measured walk of a four-digit listing.
On all 60 reviews this run: `review.language` = `null`, `review.original_language` = `null`, `author.location` = `"US"`, `verified` = true, `ext.tiktokshop.is_incentivized_review` = false. Country lives on the reviewer. Language is not a country code, and it is not English — live is `null`.
Worked review (page 1, first): id `7667988866325907213`, rating `{value: 5, max: 5}`, 336-character text starting "These nail clippers are really nice and work great.", author Ash, `published_at` `2026-07-29T16:33:12.322Z`. One product, US region. Do not generalise "every product has 1,125 reviews".
```bash
curl "https://www.socialcrawl.dev/v1/tiktokshop/product/reviews?url=https://www.tiktok.com/shop/pdp/cat-nail-clipper-by-potaroma-adjustable-sizes-built-in-file-safe-for-kittens-cats/1731578642912612516" \
-H "x-api-key: $SOCIALCRAWL_API_KEY"
```
Trimmed first review. Live `review.text` is 336 characters; the string below is cut after the opening sentence.
```json
{
"review": {
"id": "7667988866325907213",
"entity_id": "1731578642912612516",
"rating": { "value": 5, "max": 5 },
"text": "These nail clippers are really nice and work great. …",
"author": { "name": "Ash", "location": "US" },
"language": null,
"verified": true,
"published_at": "2026-07-29T16:33:12.322Z",
"ext": {
"tiktokshop": { "is_incentivized_review": false }
}
}
}
```
## Does a TikTok Shop product API return the same object from search and shop listing?
Same canonical shape. Not identical density.
Product `1729587769570529799` (3 Bottles of Goli Ashwagandha & Vitamin D Gummy) sat on shop catalogue page 1 **and** on `GET /v1/tiktokshop/search?query=goli ashwagandha` (11 hits, 1 credit, `req-bOgwplfdH63zwU6l`, 2026-09-08). Shared keys and types: `id`, `url`, `title`, `description`, `seller`, `brand`, `price` `{current, original, currency}`, `rating` `{average, count}`, `image_urls`, `availability`, `reviews_count`, `ext`. That is the TikTok Shop product data you can parse once.
[JSON:API](https://jsonapi.org/format/#document-resource-objects) treats the same resource as the same object whichever collection it appeared in; clients join on that identity. A parser that already handles the shop-list product should reuse it on search. Official TTS already keys products by one product ID across Get Product List and Get Product Detail ([Product API overview](https://partner.tiktokshop.com/docv2/page/products-api-overview)).
| Field | Shop listing | Search hit |
| --- | --- | --- |
| `id` | `1729587769570529799` | `1729587769570529799` |
| `title` | 3 Bottles of Goli Ashwagandha & Vitamin D Gummy - Mixed Berry, KSM-66, Vegan, Plant Based, Non-GMO, Gluten & Gelatin Free | same string |
| `seller` | `Goli Nutrition` | `Goli Nutrition` |
| `price` | `{current: 44.98, original: 57, currency: "USD"}` | `{current: 44.98, original: 57, currency: "USD"}` |
| `rating.average` | 4.7 | 4.7 |
| `rating.count` | 71730 | `null` |
| `reviews_count` | 71730 | `null` |
| `ext.sold_count` | 864108 | 864108 |
| `ext.sku_id` | `1729587788239049223` | `null` |
Not every field is populated on every lane. The object is the same shape under the unified schema. Search adds merchandising (`trust_label`, `promotion_labels`, `category_breadcrumb`, `video_url`, `discount_percent` under `ext.tiktokshop`). The shop list adds the shop profile (id, name, url, rating, region, followers, product count). Review count is populated on the shop row and was **null** on this search hit. Search used to return raw extra fields, not the product object.
Creator showcase is a different shape entirely — `{post, computed}`, commerce on `post.ext.commerce`. Do not treat showcase rows as this object.
Second search, page-size check, 2026-09-08: `query=phone case` → **34** items, `has_more: true`, 1 credit, `req-0Vcs8H2Scf1HPTYw`, first seller `"TECHGODAS"`, first price `{current: 11.99, original: 21.99, currency: "USD"}`. Same canonical keys as the shop list.
```bash
curl "https://www.socialcrawl.dev/v1/tiktokshop/search?query=goli%20ashwagandha" \
-H "x-api-key: $SOCIALCRAWL_API_KEY"
```
## What extra fields does a TikTok Shop product now carry?
On the listing row, description is null. On product detail, it is the seller's copy.
`GET /v1/tiktokshop/product?product_id=1729587769570529799` (no `url`) returned HTTP 200, 1 credit, cache miss, 4.54 s, `req-lr9dtYDvozKSKMOz`, 2026-09-08. Lookup by `product_id` works. Reviews already accepted both identifiers.
`String(data.product.description).length` = **1288**. 184 words, 8 lines. It starts "Goli Nutrition has sold over 90 Million bottles of gummy vitamins." That is seller copy, not a one-word category label. Labeled before: description used to be `"Health"` / `"Beauty"`. `ext.tiktokshop.category` is still `"Health"` as a separate field — the category did not disappear.
Also on this detail call: `availability: "in_stock"` · `discount_percent: 21` · `shipping_fee: 0` · `free_shipping: true` · delivery `"Sep 11 - Sep 14"` (days 4–7) · `variations` length **1** (`id` `1729587788239049223`, title `Specifications: Default`) · 8 `specifications` (Brand, Container type, Flavor, Product form, Region of origin, Age warning, Manufacturer, Ingredients) · seller location `"United States of America"` · seller rating **4.6** · `seller_sold_count` **5,726,901** · `stock` 237,896 · brand `"Goli"` · 6 `image_urls`. Credits unchanged at 1; fields richer.
This product has **one** SKU in `variations` — the field exists; do not read a variant matrix out of this capture. A catalogue walk does not give you the 1,288-character copy. Product-detail region is US-only (documented); a GB-only listing would 404 — not probed this run.
```bash
curl "https://www.socialcrawl.dev/v1/tiktokshop/product?product_id=1729587769570529799" \
-H "x-api-key: $SOCIALCRAWL_API_KEY"
```
Trimmed detail. Full `description` is 1,288 characters; the string below is the opening sentence only. On this endpoint `ext` sits on the product.
```json
{
"product": {
"id": "1729587769570529799",
"description": "Goli Nutrition has sold over 90 Million bottles of gummy vitamins. …",
"seller": "Goli Nutrition",
"brand": "Goli",
"availability": "in_stock",
"reviews_count": 71730,
"variations": [
{ "id": "1729587788239049223", "title": "Specifications: Default" }
],
"ext": {
"sold_count": 864108,
"tiktokshop": {
"category": "Health",
"discount_percent": 21,
"stock": 237896,
"free_shipping": true,
"shipping_fee": 0,
"delivery_estimate": "Sep 11 - Sep 14",
"delivery_days_min": 4,
"delivery_days_max": 7,
"seller_location": "United States of America",
"seller_rating": 4.6,
"seller_sold_count": 5726901
}
}
}
}
```
## How do I page a TikTok Shop catalogue from Python?
A TikTok Shop Python API walk is a `requests` loop that prints `len(items)` and `credits_used` across pages. One billed page per call. Pass `pagination.next_cursor` back as `cursor`. Stop when `has_more` is false.
Auth is `x-api-key`. Limits: 600 requests/minute, 50 concurrent ([pricing](/pricing)). No TikTok developer app, no OAuth, no `research.data.basic`.
```python
BASE = "https://www.socialcrawl.dev"
SHOP = "https://www.tiktok.com/shop/store/goli-nutrition/7495794203056835079"
headers = {"x-api-key": os.environ["SOCIALCRAWL_API_KEY"]}
cursor = None
seen = set()
credits = 0
while True:
params = {"url": SHOP}
if cursor:
params["cursor"] = cursor
r = requests.get(
f"{BASE}/v1/tiktokshop/products",
params=params,
headers=headers,
timeout=60,
)
r.raise_for_status()
payload = r.json()
items = payload["data"]["items"]
pag = payload.get("pagination") or {}
credits += payload.get("credits_used") or 0
for row in items:
seen.add(row["product"]["id"])
print(len(items), payload.get("credits_used"), len(seen), pag.get("has_more"))
if not pag.get("has_more"):
break
cursor = pag.get("next_cursor")
if not cursor:
break
print("unique", len(seen), "credits", credits)
# 20 1 20 True
# 20 1 40 True
# 12 1 52 False
# unique 52 credits 3
```
Expected on this fixture, 2026-09-08: 20 + 20 + 12 = **52** unique, **3** credits.
The same cursor pattern works on reviews. Reviews and search also accept `page`. Do not loop 1,125 reviews in the snippet — cap pages, or stop on `has_more`. This harvest stopped reviews at 6 pages on purpose (`has_more` still true, 60 of 1,125).
## Do missing handles and bad regions bill a credit?
No. Three captured misses on 2026-09-08, all unbilled:
| Call | HTTP | Latency | Credits | `request_id` |
| --- | ---: | ---: | ---: | --- |
| showcase `handle=this-handle-should-not-exist-xyz123` | **404** `RESOURCE_NOT_FOUND` | **1.21 s** | **0** | `req-5inAB63a9hVESzZr` |
| reviews `region=ZZ` | **400** `INVALID_REQUEST` | **66 ms** | **0** | `req-nRDOu4aMNZMD2z0h` |
| product with neither `url` nor `product_id` | **400** `INVALID_REQUEST` | **0.32 s** | **0** | `req-bJRt7cq1Qj77MkCB` |
Verbatim messages:
- Bad region: `Invalid value for 'region': 'ZZ'. Allowed values: US, GB, DE, FR, IT, ID, MY, MX, PH, SG, ES, TH, VN, BR, JP, IE.`
- Missing params: `Missing required parameter(s): one of url, product_id. This endpoint accepts: one of (url, product_id).`
- Missing handle: `The requested resource was not found on the platform.`
[RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-404-not-found) treats 404 as a missing resource, not an empty list. The same showcase path with real handle `mrtiktokreviews` billed 1 credit (20 items, `has_more: true`). A missing handle is not an empty 200.
Labeled before (2026-09-07 note, not re-measured today): missing handle used to empty-200; a bad region on reviews used to retry 4× then 500. Live it rejects before billing.
```bash
curl -i "https://www.socialcrawl.dev/v1/tiktokshop/user/showcase?handle=this-handle-should-not-exist-xyz123" \
-H "x-api-key: $SOCIALCRAWL_API_KEY"
```
```json
{
"success": false,
"error": {
"type": "RESOURCE_NOT_FOUND",
"message": "The requested resource was not found on the platform.",
"status": 404
}
}
```
## How do you start using this?
1. Get a key. The free plan is [100 credits, no card](/pricing). Send it as `x-api-key`. One key covers the shop catalogue path, reviews, search, and product detail.
2. Run the Goli Nutrition shop GET from the first example. Print `len(items)` and `credits_used`. Try the same GET in the [visual explorer](/explorer) before writing a single line.
3. Then open the live catalogue at [`/platforms/tiktokshop`](/platforms/tiktokshop), the endpoint docs at [`/docs/tiktokshop`](/docs/tiktokshop), and the [which-API comparison](/blog/best-tiktok-shop-apis-2026) if you landed here looking for a tiktok shop scraper, a scrape tiktok shop recipe, or a tiktok shop data api roundup.
Creator showcase is already paged and was unchanged this wave. `GET /v1/tiktokshop/user/showcase?handle=mrtiktokreviews` returned **20** items, `has_more: true`, 1 credit, `req-RiOpn1XxifwZ3XE6`, 2026-09-08. Item shape is `{post, computed}` — a PostList, not the ProductList used by shop and search. First commerce sits on `post.ext.commerce`: price 15 / 30 USD, rating 4.4 / 35,223, sold 524,359. One page is enough; not walked further.
Same-wave family, not Shop: [150 TikTok followers per credit](/blog/tiktok-api-more-per-credit).
Reproduce it: with any SocialCrawl key, `GET https://www.socialcrawl.dev/v1/tiktokshop/products?url=https://www.tiktok.com/shop/store/goli-nutrition/7495794203056835079`, then follow `pagination.next_cursor`. On 2026-09-08 that walk was 52 of 52 products, 3 credits, 1 credit a page. Try the same GET in the visual explorer.
## Frequently asked questions
### Can a TikTok Shop API return a whole shop catalogue?
Yes on this fixture. `GET /v1/tiktokshop/products` on Goli Nutrition returned **52 of 52** products over **3 pages for 3 credits** (20 + 20 + 12), `data.total=52`, `has_more` false on page 3. Live 2026-09-08. One shop, not a promise that every shop is 52 products. Official Research shop-info is metadata, max 10, not a catalogue.
### How many products does one TikTok Shop API credit return?
**20** on a full shop-list page this run (last page **12**), live 2026-09-08. 1 credit a page. Three pages = 52 unique products, 3 credits. Page size is not a caller `limit`. Print `len(items)`.
### How many TikTok Shop reviews can I pull per credit?
**10** reviews a page, 1 credit. This harvest pulled **60 distinct over 6 pages, 6 credits**, out of **1,125** reported on page 1. `has_more` was still true — the corpus was not exhausted. Official Research reviews max 10 per page and EU-only.
### Do I need a TikTok Shop Partner account to read public products?
No. Partner / TTS API is seller-gated OAuth for authorized shops. This walk used `x-api-key` on public shop and product URLs. For Partner-versus-public and the vendor table, see [which TikTok Shop API](/blog/best-tiktok-shop-apis-2026).
### Can I look up a TikTok Shop product by product_id, not just URL?
Yes. `GET /v1/tiktokshop/product?product_id=1729587769570529799` (no URL) returned 200, 1 credit, 2026-09-08. Description **1,288** chars, `in_stock`, 21% off, shipping 0, delivery Sep 11–14. Reviews already accepted both identifiers.
### Do search and shop listing return the same product object?
Same canonical shape. Product `1729587769570529799` matched on id, title, seller, and price `{current: 44.98, original: 57, currency: "USD"}` from `query=goli ashwagandha` and from the Goli shop list, live 2026-09-08. Density is not identical: `reviews_count` was 71730 on the shop row and null on that search hit.
### What happens if the TikTok Shop handle does not exist?
**404** `RESOURCE_NOT_FOUND` in **1.21 s, 0 credits**. A malformed `region=ZZ` is **400** in **66 ms, 0 credits**, naming the 16 accepted regions. A missing handle is not an empty 200.