# TikTok video search API: region on 30/30, 1 credit (https://www.socialcrawl.dev/blog/tiktok-video-search-api) > TikTok video search API: 30/30 cooking rows labeled at 1 credit. region=ES still returned 5 non-ES videos — filter country client-side on post.ext.region. A TikTok video search API returns the country on every video (`post.ext.region`, ISO 3166-1 alpha-2). Production `GET /v1/tiktok/search?query=cooking` at `https://www.socialcrawl.dev` on 2026-09-18 returned **30** videos with `post.ext.region` filled **30/30** — US 24, CA 2, BE / DK / IT / GB 1 each. **1 credit**, a cursor, no redirect. Request `req-bwS0R7rf6fSolozv`. [TikTok Search](https://www.tiktok.com/search) does not expose ISO-2 on each video as an API field. The [Research API Query Videos](https://developers.tiktok.com/docs/en/research-api-specs-query-videos) spec can return `region_code` only after an approved research app, a required ≤30-day date window, and a POST boolean query — not a commercial `GET ?query=`. That official dead-end is covered in [TikTok data without the official API](/blog/tiktok-data-without-api). **Stack:** curl · `GET https://www.socialcrawl.dev/v1/tiktok/search` · header `x-api-key`. No TikTok developer app. Shipped 18/09/2026 ([changelog](/changelog#tiktok-search-labeled-2026-09-18)). The 1-credit ~30-row page is unchanged in price — this ship **labels** those rows ([unfiltered ~30-row page at 1 credit](/blog/tiktok-api-more-per-credit)). Three captured 200s sit below. After those comes the `region=` mix that still is not a filter. A tall rounded video tile with a clay-amber map pin on the corner, showing country labeled on each TikTok video search row. ## How does TikTok video search work as an API? One GET with `query=` returns about 30 videos, each already carrying `post.ext.region` ([ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)), at **1 credit**, and still paginates with `cursor`. A TikTok keyword search API is this call: keyword in `query=`, country already on the row. No `region=` was sent. The 2026-09-18 production harvest hit `https://www.socialcrawl.dev` with `Cache-Control: no-cache` and `--max-redirs 0`. A 3xx would have failed. Live call: ```bash curl -s --max-redirs 0 \ -H "x-api-key: $SOCIALCRAWL_API_KEY" \ -H "Cache-Control: no-cache" -H "Pragma: no-cache" \ "https://www.socialcrawl.dev/v1/tiktok/search?query=cooking" ``` HTTP **200** at 2026-09-18T22:07:19Z, **30** videos, region **30/30**, `credits_used: 1`, `cached: false`, `dropped: 0`, `pagination.has_more: true`, `next_cursor` present (1,711 characters). Request `req-bwS0R7rf6fSolozv`. Unique video ids 30/30 — no in-page repeats. Trimmed envelope from that response (cursor string cut; `author_following` was **absent** on this path, not null): ```json { "success": true, "platform": "tiktok", "endpoint": "/v1/tiktok/search", "credits_used": 1, "cached": false, "request_id": "req-bwS0R7rf6fSolozv", "pagination": { "has_more": true, "page_size": 30 }, "data": { "dropped": 0, "items": [ { "post": { "id": "7678333675544513825", "url": "https://www.tiktok.com/@alya.cookss/video/7678333675544513825", "author": { "username": "alya.cookss", "display_name": "Alya.Cooks" }, "ext": { "region": "BE", "author_id": "7455670780618359830", "author_followers": 94159, "download_count": 1194 } } } ] } } ``` Country mix on that unlabeled `query=cooking` page: | `post.ext.region` | Videos | | --- | ---: | | US | 24 | | CA | 2 | | BE | 1 | | DK | 1 | | IT | 1 | | GB | 1 | | **Filled** | **30/30** | | ES | 0 | Read the nested path: `data.items[i].post.ext.region`. That leaf is **the unified schema** — a canonical post, not a one-off geo blob. The first row was `alya.cookss` / BE, not a US video. Unlabeled keyword search is global; the labels still fill. This page had **24 unique usernames** across 30 videos. Creator leaves on this 1-credit page: `username` / `author_id` / `author_followers` / `download_count` **30/30**; `author_following` **0/30 (key absent)**. Do not code against `author_following` on the cheap page; it was missing today. The United Kingdom row is **GB**, not UK. Docs also list optional `date_posted` (`yesterday | this-week | this-month | last-3-months | last-6-months | all-time`) and `sort_by` (`relevance | most-liked | date-posted`). This harvest did not send them, so treat those as documented params, not measured here. [TikTok's public search page](https://www.tiktok.com/search) has no ISO-2 API field. That is why TikTok video search SERPs as the app, and this section is the API. This is not `GET /v1/tiktok/search/users?country=`, not `trending?region=&feed=local`, and not `hashtags/popular?countryCode=`. A country breakout board, with TikTok posts, sounds, hashtags, and Google searches in one response, is [a social media trends API call](/blog/whats-trending-by-country). Live param table: [`GET /v1/tiktok/search`](/platforms/tiktok/search). Optional Python twin of the same call: ```python r = requests.get( "https://www.socialcrawl.dev/v1/tiktok/search", params={"query": "cooking"}, headers={"x-api-key": os.environ["SOCIALCRAWL_API_KEY"]}, timeout=60, ) payload = r.json() items = payload["data"]["items"] print(r.status_code, payload["credits_used"], len(items)) print([row["post"]["ext"]["region"] for row in items]) ``` A fanned stack of video tiles with one clay-amber map pin on the front tile, showing how to filter a TikTok video search by country. ## How do I filter TikTok videos by country? The SocialCrawl API does not drop other countries for you. You filter `post.ext.region` client-side. TikTok videos by country, or a TikTok search by country in this product, means ISO-2 on each video — you keep the code you want. `region=` only sets the proxy — it does not filter. On this harvest, `region=ES` still returned non-ES rows. ```bash curl -s --max-redirs 0 \ -H "x-api-key: $SOCIALCRAWL_API_KEY" \ -H "Cache-Control: no-cache" -H "Pragma: no-cache" \ "https://www.socialcrawl.dev/v1/tiktok/search?query=receta®ion=ES&limit=30" ``` HTTP **200** at 2026-09-18T22:07:42Z, **30** videos, region **30/30**, `credits_used: 10` (matches `ceil(30/4)+2`), **no `next_cursor`**, `has_more: false`, `dropped: 0`, `cached: false`. Request `req-ucdIYc9pt7XQPkIf`. This is the path where `author_following` **is** present. Trimmed ES row: ```json { "success": true, "platform": "tiktok", "endpoint": "/v1/tiktok/search", "credits_used": 10, "cached": false, "request_id": "req-ucdIYc9pt7XQPkIf", "pagination": { "next_cursor": null, "has_more": false, "page_size": 30 }, "data": { "dropped": 0, "total": 30, "items": [ { "post": { "id": "7684609864760364320", "url": "https://www.tiktok.com/@sofiatecocina/video/7684609864760364320", "author": { "username": "sofiatecocina" }, "ext": { "region": "ES", "author_id": "6862032819960693766", "author_followers": 417506, "author_following": 91, "download_count": 1 } } } ] } } ``` Mix on that `query=receta®ion=ES&limit=30` page: | `post.ext.region` | Videos | | --- | ---: | | ES | 25 | | CO | 2 | | PL | 1 | | DE | 1 | | US | 1 | | **Filled** | **30/30** | | ES share | **25/30 (83.3%)** | | Unique usernames (page) | 27 | | Unique ES creators | **22** | Five rows prove `region=` did not filter: | username | `post.ext.region` | video id | | --- | --- | --- | | rubi.chef | CO | 7617623218760862996 | | isabellarodriguezu | CO | 7632395980767366418 | | officialepiceats | PL | 7672057190374460705 | | tagesrezept | DE | 7643897506845248800 | | nurysule | US | 7584637852428897550 | Keep rows where `post.ext.region` equals `"ES"`: 25 videos → **22 unique ES usernames**. That filtered page is already a country creator list. Filtering is free. `limit=30` billed **10** for the labeled page; you drop the five non-ES rows yourself. Unique video ids were 30/30. For current exact follower count, bio and links, pass the username to `GET /v1/tiktok/profile` (1 credit) or `POST /v1/prism/profiles` (1 credit per resolved profile, up to 50). Deduplicate usernames across pages; this bulk call has no cursor to walk. `receta` plus `region=ES` is a Spanish-language cooking query with a Spain proxy. A reasonable seed, not a census. A Korea beauty query, or the same `receta` at a different hour, will mix differently. 25/30 ES on this page is a snapshot. [Research API Query Videos](https://developers.tiktok.com/docs/en/research-api-specs-query-videos) uses `region_code` as a **server-side** WHERE clause on an academic POST with a required 30-day window. We label every row; you filter. `post.ext.region` is the country TikTok registered the video to (typically the creator’s account country at post time) — not viewer country, not audience geo, and not user-search `country=`. ## How many credits does a labeled TikTok video search cost? Omitting `limit` is still **1 credit**, still labeled, still paginates. `limit=` is the opt-in bulk labeled page: a single shot, no cursor. The 1-credit price is the 2026-09-04 density ship ([unfiltered ~30-row page at 1 credit](/blog/tiktok-api-more-per-credit)); this 18/09 ship adds the label. Third captured call, same headers, no `limit`: ```bash curl -s --max-redirs 0 \ -H "x-api-key: $SOCIALCRAWL_API_KEY" \ -H "Cache-Control: no-cache" -H "Pragma: no-cache" \ "https://www.socialcrawl.dev/v1/tiktok/search?query=receta®ion=ES" ``` HTTP **200** at 2026-09-18T22:08:02Z, **30** videos, region **30/30**, `credits_used: 1`, **cursor present**, `has_more: true`, `dropped: 0`. Request `req-BDe1R2pa5B09A6xA`. Mix: ES 26, MX 2, US 1, CO 1 → **4 non-ES**, 24 unique usernames, 20 unique ES creators. `region=` still does not filter when you omit `limit`. Omit `limit` when you want to walk pages at 1 credit each and stop when you have enough matching rows. Pass `limit=30` when you want one 30-row labeled shot and will filter after the call. This harvest did not re-run four 1-credit pages on production — do not assume a 4-page unique count. | Call | Product claim | Observed `credits_used` | | --- | --- | ---: | | No `limit`, `query=cooking` | 1-credit page of ~30 | **1** | | No `limit`, `query=receta®ion=ES` | 1-credit page of ~30 | **1** | | `limit=30` | `ceil(30/4)+2` = 10 | **10** | | `limit=60` | formula → 17 | **not run** | | `limit=90` | formula → 25 | **not run** | | `limit=120` | formula → 32 | **not observed** (504, refunded) | `limit` is documented as 1–120 and rounds up to 30 / 60 / 90 / 120. Credits follow `ceil(n/4)+2` → 10 / 17 / 25 / 32. Only `limit=30` → **10** was observed this run. 17 / 25 / 32 are the documented formula, not this harvest. Two production `limit=120` attempts (`req-UCkQA3eOkYqcSEJH`, retry `req-7WXb1xv9LVXb8ljT`) returned HTTP **504 `UPSTREAM_ERROR`**, `credits_used: 0`. Trimmed retry body: ```json { "success": false, "error": { "type": "UPSTREAM_ERROR", "message": "The upstream data provider timed out. Your credits have been refunded.", "status": 504, "doc_url": "https://www.socialcrawl.dev/docs/errors#upstream-error" }, "credits_used": 0 } ``` Docs also say repeats are removed after billing (a 120 page can land a little under 120 unique) and unfilled slots are refunded. **Neither unique-count nor unfilled-slot refund was demonstrated this run**, because the call never returned 200. The 504s *were* refunded. Prefer `limit=30` (observed **10** credits, 30 videos, no cursor) or omit `limit` (1 credit + cursor). Creator leaves on each path — this is what you get: | Leaf | cooking (1 cr) | receta ES `limit=30` (10 cr) | receta ES no limit (1 cr) | | --- | --- | --- | --- | | `post.author.username` | 30/30 | 30/30 | 30/30 | | `post.ext.author_id` | 30/30 | 30/30 | 30/30 | | `post.ext.author_followers` | 30/30 | 30/30 | 30/30 | | `post.ext.author_following` | **0/30 (key absent)** | **30/30** | **0/30 (key absent)** | | `post.ext.download_count` | 30/30 | 30/30 | 30/30 | | `post.ext.region` | 30/30 | 30/30 | 30/30 | Cache miss bills; cache hit 0; empty/hard fail refunded; 400 never charged. All three 200s this run were misses. Harvest spend on those 200s was **12 credits** (1 + 10 + 1). Both 504s billed 0. ## Is video-search `region=` the same as user-search `country=`? No. These “country” ideas get mixed constantly. This endpoint is ISO-2 on each **video** row, and you filter. 1. **Video registered region** — `post.ext.region` on `GET /v1/tiktok/search` (this post). You filter. 2. **Request routed through a country (proxy)** — `region=` on video search. Sets the proxy only. Harvest: 5 non-ES on the ES `limit=30` page; 4 non-ES on the no-limit ES page. 3. **Creator’s public account region** — `author.location`. `GET /v1/tiktok/search/users?country=` keeps only matches, **5 credits per matching creator**, default 8 max 20, no cursor. That product is [TikTok user search by country: 3 KR hits, 15 credits](/blog/find-tiktok-creators-by-country). This harvest did not call it. 4. **Audience geography** — `GET /v1/tiktok/user/audience` on a known handle. Not search. | | omit `limit` (this harvest) | `limit=30` (this harvest) | `GET /v1/tiktok/search/users?country=` (sibling / docs) | | --- | --- | --- | ---: | | Credits | **1** per page | **10** | **5** per **delivered** creator | | Rows | 30, paginates with cursor | 30, **no cursor** | 8 default / 20 max, no cursor | | Country leaf | `post.ext.region` on each **video** (30/30) | same | `author.location` on each **creator**; server keeps only matches | | `region=` / `country=` | `region=` sets **proxy only** (4 non-ES on receta/ES) | 5 non-ES on receta/ES | `country=` **filters** | If you want creators whose **account** is in KR, use the sibling. If you want cooking **videos** labeled ES, use this endpoint and filter `post.ext.region`. Related, and not this call: `include=profile` hydrates a global user-search page; `GET /v1/tiktok/profile` looks up a known handle; `hashtags/popular?countryCode=` is a [TikTok Creative Center](https://ads.tiktok.com/business/creativecenter) board; `trending?region=&feed=local` is a For-You feed. None of them are keyword video search. ## How do you start using this? 1. Get a SocialCrawl key. The free plan includes [100 credits, no card](/pricing). Auth is `x-api-key` — **one API key**. No TikTok developer app, no Research API application. 2. Run the cooking curl above (`query=cooking`, no `limit`). Expect about 30 rows, `post.ext.region` on each, 1 credit, a cursor. Counts will not match this snapshot. The shape and the credit charges should match. 3. To keep one country: add `region=` only as a proxy hint, then keep rows where `post.ext.region` equals `"ES"` (or KR, GB, …). Do not expect the API to drop the rest. 4. After that: the [explorer](/explorer) to see the payload before writing a single line; the [TikTok video search endpoint](/platforms/tiktok/search) for the live param table; the [TikTok docs](/docs/tiktok) and the [TikTok platform catalogue](/platforms/tiktok) for the rest of the reads. If you want **creators** whose account region matches, that is [5 credits per matching creator](/blog/find-tiktok-creators-by-country). The scrape-vs-official-API landscape lives in [How to scrape TikTok in 2026](/blog/how-to-scrape-tiktok-2026) — not this endpoint. MCP and Agent Skills wrap the same `x-api-key` surface if an agent should fire the GET. Replay the cooking curl first. ## Frequently asked questions ### Can I run a TikTok search by country on videos? Yes — as a **label plus client-side filter**, not as a server-side drop. `GET /v1/tiktok/search` returns `post.ext.region` ([ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)) on every video. Keep the rows you want. This is not `GET /v1/tiktok/search/users?country=` (that keeps matching **creators** at 5 credits each). ### How do I get TikTok videos by country from keyword search? Call `GET /v1/tiktok/search?query=…`, read `post.ext.region` on each row, keep one ISO-2. This harvest: `query=receta®ion=ES&limit=30` returned 25 ES of 30 (22 unique ES usernames) at 10 credits. `region=` is optional proxy, not the filter. ### Does `region=` filter TikTok search results? No. `region=` only sets the proxy. On 2026-09-18, `region=ES&limit=30` still returned 5 non-ES rows (CO 2, PL, DE, US). The no-limit ES page still returned 4 non-ES (MX 2, US, CO). Filter on `post.ext.region`. ### Does the 1-credit TikTok video search page include country labels? Yes. Omitting `limit` is still ~30 rows, still labeled (`post.ext.region` 30/30 on both 1-credit pages this run), still **1 credit**, still paginates with `cursor`. The 1-credit price is the 2026-09-04 density ship — this 18/09 ship adds the label. See the [unfiltered ~30-row page at 1 credit](/blog/tiktok-api-more-per-credit). ### What happens if I pass `limit=120`? Documented: rounds to a 120-row single shot, no cursor, billed `ceil(120/4)+2` = **32**, repeats stripped after billing, unfilled slots refunded. **This run:** two production attempts returned HTTP **504**, `credits_used: 0`. Do not quote unique / ES / creator counts for a 120 page. Prefer `limit=30` (observed **10** credits, 30 videos, no cursor) or omit `limit` (1 credit + cursor). ### How is this different from finding TikTok creators by country? Video search labels **videos** with `post.ext.region` at 1 credit (or 10 at `limit=30`); you filter. User search `GET /v1/tiktok/search/users?country=` keeps matching **creators** at **5 credits per delivered row**, max 20, no cursor. See [TikTok user search by country: 3 KR hits, 15 credits](/blog/find-tiktok-creators-by-country). This harvest did not call that endpoint.