# Threads Keyword Search: 75% On-Topic for 1 Credit (https://www.socialcrawl.dev/blog/threads-keyword-search) > Live 2026-09-08: cari ai automation returned 20 posts, 75% multi-term hits, 1 credit (was 3). Pair-only expansion; expand=false stayed empty and free. A Threads keyword search on an unknown three-word phrase now stays on the words you typed. Live 2026-09-08 UTC, `GET /v1/threads/search?query=cari+ai+automation` returned **20** posts, **15/20 (75%)** whole-word multi-term hits, **1 credit** (cache-cold, 5,604 ms client wall-clock). The warning named two adjacent pairs (`cari ai`, `ai automation`); **1 of 2** returned posts. Same query with `expand=false`: **0** posts, **0 credits**. Ship note 07/09/2026 on that query: 59 posts / 25% multi-term / 3 credits, then 20 / 75% / 1 credit. Live **matches the after**. We did not re-measure the before. Official meta threads search is `GET graph.threads.net/v1.0/keyword_search` — [2,200 queries per rolling 24 hours](https://developers.facebook.com/documentation/threads/keyword-search), App Review required. This post is the public-read envelope on the SocialCrawl API — matching behaviour Meta never documents. For OAuth and App Review, see [Threads API in 2026](/blog/threads-api). This is not the comments, carousel, or yield tour — that threads search api post lives at [Scrape Threads](/blog/threads-scraper). **Stack:** curl · Python `requests` · `GET https://www.socialcrawl.dev/v1/threads/search` · header `x-api-key`. Print `len(items)` and `credits_used`. Free plan: [100 credits, no card](/docs/credits). Catalogue: [`/docs/threads`](/docs/threads) · [`/platforms/threads`](/platforms/threads). Five live envelopes billed **4 credits** (`1 + 0 + 1 + 1 + 1`). Pair-only expansion, exact-phrase opt-out, unsplit known phrases, and a short-term word-boundary check. A magnifying glass over a looping ribbon selecting a tight cluster of matching posts, the visual for a Threads keyword search that stays on-topic. ## How does pair expansion change Threads keyword search results? Threads does not score your query like a search index. It matches against topic tags it already knows. An unrecognised phrase used to come back empty — or, on the pre-07/09 default path, fall back to single words and fill the page with whatever those tokens happened to hit. The threads keyword search api is still `GET /v1/threads/search`. What changed is the fallback: an unknown phrase now expands into **adjacent word pairs only** — **1 credit per pair that returned posts** — and the merge ranks by how many of *your* terms each caption still carries. ```bash curl -s -H "x-api-key: $SOCIALCRAWL_API_KEY" \ "https://www.socialcrawl.dev/v1/threads/search?query=cari%20ai%20automation" ``` Live (`req-B1nKGNBEA20fk0U2`, HTTP 200, `cached: false`, 5,604 ms): **20** posts, **1 credit**, `pagination.has_more=false`, no `pagination.next_cursor`. Expanded pages do not paginate. The warning, verbatim: > No Threads post matched "cari ai automation" as a phrase, so the query was expanded into 2 narrower searches (cari ai, ai automation) and 1 of them returned posts. Results are merged, de-duplicated and ordered by how many of your terms each post contains. You are billed 1 credit per search that returned posts, and an expanded response is a single page with no pagination cursor. Send expand=false to search the exact phrase only. | | Ship note 07/09/2026 (before → after) | Live 08/09/2026 | | --- | --- | --- | | Posts | 59 → 20 | **20** | | Multi-term (≥2 of cari / ai / automation in caption) | 15 → 15 (25% → 75%) | **15 / 20 = 75%** | | Credits | 3 → 1 | **1** | | Relaxed searches | up to 4 → at most 2 pairs | **2** named; **1** returned | A post counts as a multi-term hit only if **at least two** of the typed terms appear as **whole words** in `post.content.text`. Whole word means a left and right boundary (`(? ## When should you send expand=false instead of relaxing the phrase? Default `expand=true` is the recall path: if Threads has never heard the phrase, search the adjacent pairs. Send `expand=false` when you want the exact phrase Threads already files as a topic — empty is fine. ```bash curl -s -H "x-api-key: $SOCIALCRAWL_API_KEY" \ "https://www.socialcrawl.dev/v1/threads/search?query=cari%20ai%20automation&expand=false" ``` Live (`req-9e5zpulaoFDfH4WE`, HTTP 200, `cached: false`, 1,147 ms): ```json { "success": true, "platform": "threads", "endpoint": "/v1/threads/search", "credits_used": 0, "cached": false, "request_id": "req-9e5zpulaoFDfH4WE", "pagination": { "has_more": false, "page_size": 0 }, "data": { "items": [], "total": 0, "dropped": 0 } } ``` **0** posts, **0 credits**. Threads has never heard of `cari ai automation` as a phrase. An empty exact-phrase call does not bill. Same empty-and-free behaviour was already true on 04/09; what changed on 07/09 is the *default* path stopped falling back to single tokens. Leave `expand` at default when you want recall of an unknown phrase. Set `expand=false` when a pair-merge would mix in posts that only share a neighbouring pair, not the phrase you typed. Two linked tile pairs standing in for adjacent word-pair expansion on a Threads keyword search, with a long chain of single tiles left unused. ## Does word count decide whether a Threads query splits? No. Three words is not a split trigger. A phrase Threads already knows returns one window, **1 credit**, no `_warnings`, and pagination stays on. A phrase it does not know expands into pairs. This harvest called two recognised three-word phrases; neither split. ```bash curl -s -H "x-api-key: $SOCIALCRAWL_API_KEY" \ "https://www.socialcrawl.dev/v1/threads/search?query=machine%20learning%20engineer" ``` `query=machine learning engineer` (`req-XBrnbaOt9ttLTeJF`, 3,195 ms, `cached: false`): **20** posts, **1 credit**, no `_warnings`, `has_more=true`. Multi-term: **16 / 20 = 80%** (11 of 20 carry all three of machine / learning / engineer). ```json { "success": true, "platform": "threads", "endpoint": "/v1/threads/search", "credits_used": 1, "cached": false, "request_id": "req-XBrnbaOt9ttLTeJF", "pagination": { "has_more": true, "page_size": 20 }, "data": { "items": [ { "post": { "id": "3296195738167477136", "url": "https://www.threads.com/@codingmermaid.ai/post/C2-cI9ToQOQ", "author": { "username": "codingmermaid.ai" }, "engagement": { "likes": 324 }, "content": { "text": "Are you looking to become a machine learning engineer?" } } } ] } } ``` `query=ai automation agency` (`req-Sc5znEt3HlrQblbl`, 3,689 ms, `cached: false`): **20** posts, **1 credit**, no `_warnings`, `has_more=true`. Multi-term: **14 / 20 = 70%** (3 of 20 carry all three). ```json { "success": true, "platform": "threads", "endpoint": "/v1/threads/search", "credits_used": 1, "cached": false, "request_id": "req-Sc5znEt3HlrQblbl", "pagination": { "has_more": true, "page_size": 20 }, "data": { "items": [ { "post": { "id": "3970395697807160969", "url": "https://www.threads.com/@buildwithmano/post/DcZrdnhiPaJ", "author": { "username": "buildwithmano" }, "engagement": { "likes": 27 }, "content": { "text": "Starting my AI automation agency from scratch." } } } ] } } ``` `@papoush_nd_` is another 3-term row: "I'm looking to connect with more AI Automation Agency owners." `@stanleywangg` is a 1-term row — "My first successful automation on n8n." — still a recognised-phrase hit, not an expanded merge. A threads api search splits when Threads does not recognise the phrase, not when the query has three tokens. Docs also list `new york city` as a recognised phrase; this harvest did not call it. `cari ai automation` is the counterexample: same word count, expanded, no cursor. ## Does a short query still match `ai` inside `email`? Ship note 07/09/2026: very short Latin/digit terms (length 1–3) used to score as substrings, so `ai` hit inside `email`, `rain`, and `said`. The product now requires a left word boundary for those short terms. The live page does not contain those false hosts. We cannot show the pre-ship failure — we did not re-measure it. ```bash curl -s -H "x-api-key: $SOCIALCRAWL_API_KEY" \ "https://www.socialcrawl.dev/v1/threads/search?query=ai" ``` Live (`req-9tKz9dPbgkYe1CL0`, 3,900 ms, `cached: false`): **17** posts, **1 credit**, `has_more=true`. | Check (n=17) | Live | | --- | ---: | | Whole-word `ai` in `post.content.text` | **10 / 17** | | Whole-word `email` / `rain` / `said` in caption | **0** | | Caption where `ai` exists only inside those three hosts | **0** | | Caption-misses with topic tag `AI Threads` or `AI team` | **6** | | Caption-miss with neither | **1** (`@beewilson_`) | One other caption still has `ai` as a substring: `@meor.store` contains `ai` only inside `Aiman` / `maintain`, and also carries `topic_tag=AI team`. That is a topic-tag retrieval, not evidence the short term matched inside those tokens. Same host check on the `cari ai automation` page: **0/20** for whole-word `email` / `rain` / `said`. ```json { "success": true, "platform": "threads", "endpoint": "/v1/threads/search", "credits_used": 1, "cached": false, "request_id": "req-9tKz9dPbgkYe1CL0", "pagination": { "has_more": true, "page_size": 17 }, "data": { "items": [ { "post": { "id": "3981272493298417111", "url": "https://www.threads.com/@matthew_zorich_author/post/DdAUj0tFf3X", "author": { "username": "matthew_zorich_author" }, "engagement": { "likes": 7 }, "content": { "text": "So attended a book con recently at let me just say, a lot of authors are leaning Into AI & it's gross." } } } ] } } ``` `@matthew_zorich_author` is a whole-word `ai` hit. `@beewilson_` is the honest miss: "alright , it's Labor Day who house are u going to first ?" — no whole-word `ai`, no `AI …` topic tag. Topic tags named `AI Threads` or `AI team` still retrieve captions that never say `ai`. Longer terms and glued particles (`커피를` for `커피`, `mesinnya` for `mesin`) still match as substrings; this harvest did not re-prove those. ## What timeout do you need if you raise `limit` on Threads search? This harvest used the default window only. Every number in this section is **ship note 07/09/2026, cache-cold** — not re-measured on 08/09. `limit` (1–100) is a **collection target**, not Meta's page size (official default 25 / max 100 on [`keyword_search`](https://developers.facebook.com/documentation/threads/keyword-search)). The walk stops at the first whole window that reaches N, so the response usually overshoots. Values outside 1–100 return a free `400`. Windows run sequentially at roughly 3.4 seconds each. Ship note 07/09 cache-cold: no `limit` **3.5s / 20** posts; `limit=30` **7.3s / 37**; `limit=50` **10.2s / 55**; `limit=100` **19.5s / 111**. [HTTPX's default](https://www.python-httpx.org/advanced/timeouts/) is a `TimeoutException` after **5 seconds** of network inactivity. [Python Requests](https://requests.readthedocs.io/en/latest/user/quickstart/#timeouts) documents that omitting `timeout` can hang indefinitely. A `limit=50` call at 10.2s misses HTTPX's 5s default. The 07/09 fan-out has a **6 second** phase budget so an empty-phrase expansion still returns inside a ~10s client. Set `timeout` at **15s or more** on a threads api python client, or skip the long walk and page with `pagination.next_cursor` for the same credits. `limit` is for recognised phrases. Expanded responses have no cursor — the `cari ai automation` page in the first example is one shot, `has_more=false`. Params live on [`/docs/threads`](/docs/threads). ## How do you reproduce these calls in Python? 1. Create a key (100 free credits, no card) and send it as `x-api-key`. Auth is header-only: [`/docs/authentication`](/docs/authentication). 2. Hit the unknown-phrase call from the first example, or `machine learning engineer` if you want a paginated known phrase. 3. Python 3.10+ `requests`, `timeout=15`. Print page length and `credits_used`. Optionally score multi-term hits with the same whole-word rule. ```python BASE = "https://www.socialcrawl.dev" r = requests.get( f"{BASE}/v1/threads/search", params={"query": "cari ai automation"}, headers={"x-api-key": os.environ["SOCIALCRAWL_API_KEY"]}, timeout=15, ) r.raise_for_status() payload = r.json() items = payload["data"]["items"] print(len(items), payload.get("credits_used")) TERMS = ("cari", "ai", "automation") boundary = lambda t: re.compile( rf"(?= 2: hits += 1 print(hits, "multi-term") ``` One API key, one parser. The envelope is always `success`, `platform`, `endpoint`, `data`, `credits_used`, `credits_remaining`, `request_id`, `cached`. Canonical `Post` on every item. Next: the visual explorer at [`/explorer`](/explorer) (see the same query before writing a single line) · [`/docs/threads`](/docs/threads) · [`/platforms/threads`](/platforms/threads) · density sibling [TikTok API: 150 followers per credit](/blog/tiktok-api-more-per-credit). Reproduce the five GETs with your own key. Expect the post set to drift; a re-run will not return the same 20 ids. Expect the expansion warning, the 1-credit pair-billing, and the unsplit three-word pages to hold. ## Frequently asked questions ### How many credits does a Threads keyword search cost after 7 September 2026? 1 credit per returning ~15–20 post window. Empty exact-phrase (`expand=false` on an unknown query) bills **0**. Live 08/09: five calls billed **4** (`1 + 0 + 1 + 1 + 1`). Cache hits bill 0. Failed calls and empty results are refunded. Other Threads endpoints are unchanged. Metering: [`/docs/credits`](/docs/credits). ### What does `expand=false` do on `GET /v1/threads/search`? Exact phrase only. Live 08/09: `query=cari ai automation&expand=false` returned **0** posts, **0 credits**, HTTP 200 (`req-9e5zpulaoFDfH4WE`). Default `expand=true` relaxes an unknown phrase into adjacent pairs, bills 1 credit per pair that returned posts, and returns a single page with no pagination cursor. ### Does Threads keyword search still match short terms inside longer words? Live 08/09 `query=ai`: **0/17** captions with whole-word `email` / `rain` / `said`, and **0** captions where `ai` exists only inside those three hosts. Ship note 07/09: Latin/digit terms of length 1–3 need a left word boundary. That is an absence claim — the pre-ship baseline was not re-measured. Topic tags named `AI …` still retrieve captions that never say `ai`. One caption (`@beewilson_`, Labor Day) had neither whole-word `ai` nor an `AI …` tag. ### What timeout should I set if I pass `limit=50` or `limit=100`? Ship note 07/09 cache-cold: `limit=50` **10.2s / 55 posts**; `limit=100` **19.5s / 111**. HTTPX's 5s default will miss both. Set 15s or more, or page with `pagination.next_cursor` instead. Not re-measured on 08/09. Expanded responses have no cursor, so `limit` is for recognised phrases. ### Does pair expansion change other Threads endpoints? No. Search only (`GET /v1/threads/search`). Profile, user posts, post detail, comments, and user search are untouched.