# 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.
## 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.
## 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.