How to search Threads posts programmatically
Threads has no public search API. Meta's logged-out keyword page is the surface. GET /v1/threads/search hydrates that Top SERP as posts — id, code, text, publishedAt, engagement{}, threadId — for a flat 2 credits.
Values below are from production on October 10, 2026. Search first, then why a profile's "posts" endpoint is a short recent window, not a search.
A keyword page, not a people search
q=Threads API, limit=5 returned five posts. One of them, a year-old testing explainer, looked like this:
{
"id": "3473583137539152644",
"code": "DA0pXWyztsE",
"url": "https://www.threads.net/@thetestingacademy/post/DA0pXWyztsE",
"text": "What is API - Part-1\n.\n.\n#apitesting #api",
"publishedAt": "2024-10-07T12:16:39.000Z",
"threadId": "3473583137539152644",
"isReply": false,
"isQuote": false,
"author": {
"username": "thetestingacademy",
"displayName": "PRAMOD DUTTA",
"verified": true
},
"engagement": { "likes": 76, "replies": null, "reposts": 12, "quotes": 0 }
}
Default orderBy=relevant keeps Meta's ranking. post_dated re-sorts that fetched window newest-first. engagement ranks by likes+replies+reposts+quotes+views. Unknown orderBy is 400. The sort is not a second crawl of Threads.
The call
JavaScript:
const res = await fetch(
"https://api.captapi.com/v1/threads/search?" +
new URLSearchParams({ q: "Threads API", limit: "10", orderBy: "relevant" }),
{ headers: { Authorization: `Bearer ${process.env.CAPTAPI_KEY}` } },
);
const { data } = await res.json();
for (const p of data.results) {
console.log(p.publishedAt, p.author.username, p.engagement.likes, p.text.slice(0, 80));
}
Python:
import httpx, os
r = httpx.get(
"https://api.captapi.com/v1/threads/search",
params={"q": "Threads API", "limit": 10, "orderBy": "relevant"},
headers={"Authorization": f"Bearer {os.environ['CAPTAPI_KEY']}"},
timeout=45,
)
data = r.json()["data"]
for p in data["results"]:
print(p["publishedAt"], p["author"]["username"], p["engagement"]["likes"])
When Meta's search page itself renders "No results", the answer is an immediate empty 200 at 0 credits — a verdict, not an outage. A transient miss is retried in-request, then a 6-hour last-good page (labelled stale: true, 0 credits), then the extended scraper. A 502 UPSTREAM_UNAVAILABLE means every path missed. The endpoint does not answer 404 "No posts found" for an outage.
The trap: Top SERP is not "posts about X"
The same q=Threads API page also ranked a promotional post whose text happened to contain the letters "API" and a pasted third-party key. Meta's Top ranking is engagement and recency on the keyword page, not a semantic index of developer posts. Filter on your side. Do not treat results[0] as the canonical article. Do not log or persist secrets that ride along in post text.
Authors of matching posts are a different endpoint: /v1/threads/search-users (1 credit). Handles on that list may not contain the query — they are unique authors of posts that matched.
A profile is a short recent window
/v1/threads/user-posts is not search. @zuck on the same day returned five posts from September 28, 2026, source: "native", flat 2 credits. The first two share a threadId:
{
"id": "3996155940894885511",
"code": "Dd1MqfcG0aH",
"text": "We believe superintelligence will create significant new opportunities…",
"publishedAt": "2026-09-28T12:35:32.000Z",
"threadId": "3996155940894885511",
"isReply": false,
"engagement": { "likes": 2692, "replies": 2618, "reposts": 170, "quotes": 156 }
}
{
"id": "3996155941205275653",
"replyToId": "3996155940894885511",
"threadId": "3996155940894885511",
"isReply": true,
"engagement": { "likes": 678, "replies": 82, "reposts": 19, "quotes": 3 }
}
Rebuild a multi-part Thread from threadId / replyToId / isReply. limit caps what you receive; it cannot increase what Threads exposes. Instagram usernames often differ on Threads — known aliases remap; requestedHandle is set when they do. The profile card (/v1/threads/profile, 1 credit) for zuck was followers: 5745152, verified: true, isPrivate: false, isThreadsOnlyUser: null, bio "Mostly superintelligence and MMA takes" as both bio and a single bioFragments[] plaintext piece. Following and post counts are not on this surface. engagement.views was omitted on these hydrate rows — null/omitted, not zero. The testing-academy search hit had replies: null with likes: 76; do not coerce null replies to 0 if you are averaging.
/v1/threads/post-details (1 credit) is the permalink: same card plus comments[] when Meta embeds the reply tree and relatedPosts[] from the logged-out related module. It is not an alias of user-posts.
Bluesky's author feed has the same "don't credit a repost to the profile" problem, marked as isRepost — Bluesky profile and user-posts.
The search endpoint page has orderBy, the stale-rescue rules, and the empty-200 verdict. A free key is 100 credits — a search page and a user-posts window.