Back to blog
Threads APIThreads searchMeta API

How to search Threads posts programmatically

CaptapiOctober 10, 20263 min read
TL;DR
Threads has no public search API. Meta's Top SERP hydrate as JSON — and why the first hit is ranking, not a semantic index.
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.