Threads has no public API for other people's content. Meta ships one for your own account, and that is it. Everything else — brand monitoring, competitor tracking, research — has to come from the pages Threads serves to a visitor who is not logged in.
Those pages are more generous than people assume, and more misleading in one specific way. Both parts matter if you are building on them.
What a logged-out visitor actually gets
Open a profile without an account and Threads renders the page server-side, with the data embedded as Relay payloads inside the HTML. No browser automation is needed to read them: one HTTP request with a believable TLS fingerprint gets you the same JSON the page uses.
From those payloads you can take:
- An account's posts, paged through the same GraphQL endpoint the site calls when you scroll. Not the first 25 — a profile with 1,400 posts pages all the way down.
- Replies and reposts, each with the post being answered, so a conversation can be rebuilt from flat rows.
- A single post with its reply tree, parent links included.
- Profiles: follower count, bio, bio links, external website, verification, linked Instagram, and any e-mail or phone the owner put in the bio.
- Keyword and hashtag search, from the top, recent and tag result pages.
Every post carries text, timestamp, author, likes, replies, reposts, quotes, media URLs, links, mentions and hashtags. That is a complete social dataset, from a site with no public API, without a single browser.
The part that quietly breaks analyses
Threads' logged-out search pages return posts that do not contain your query at all.
Not a handful of edge cases — measured on 16 September 2026 for the query apify:
| Result page | Results | Actually containing the word |
|---|---|---|
| Recent | 22 | 20 |
| Top | 22 | 17 |
| Hashtag page | 26 | 7 |
The hashtag page is mostly noise. And an earlier run of the same query on the recent page returned five posts, all published in the same second, in five different languages, none mentioning the query — a global firehose, served as if it were a search result.
If you are counting brand mentions, that is not a rounding error. It is a number that is wrong by a factor of two or three, in the direction that makes your dashboard look busier.
The fix is not clever, it just has to be there: after fetching, keep only posts whose text or hashtags actually contain the query — the whole phrase, or every word of it for multi-word queries. Anything that does not pass is dropped before it is counted, stored, or billed.
def is_mention(post, query):
hay = (post["text"] + " " + " ".join(post["hashtags"])).lower()
q = query.lower().lstrip("#").strip()
if q in hay:
return True
words = [w for w in q.split() if len(w) > 1]
return len(words) > 1 and all(w in hay for w in words)
Why view counts are null, and why that is the honest answer
Threads shows a view count on a handful of posts to logged-out visitors, and hides it on the rest. Some scrapers fill the gap with a zero. That single decision quietly destroys any filter built on it: ask for "posts with at least 1,000 views" and you get an empty dataset, because almost every row was assigned a zero.
Null is the correct value for "the platform did not tell us". Filter on likes or replies, which are always there.
The same rule applies to search depth. Threads shows a logged-out visitor roughly one page per query and result type, with no cursor. You can widen the net by asking in more ways — singular and plural, with and without the hash, top and recent — and deduplicating by post id, which in practice triples the unique posts you get. What you cannot do is promise thousands of results per keyword, and anyone who does is either logged in or counting duplicates.
A scraper on a schedule is the wrong tool
Here is the pattern almost everyone builds first: point a scraper at an account, run it hourly, store everything, deduplicate later.
Run the numbers. Twenty-five posts per check, twenty-four checks a day, thirty days: 18,000 rows a month per account, of which maybe 60 are new. You paid for all 18,000, and you wrote the deduplication yourself.
The alternative is to make "what changed" the product rather than a post-processing step. A named watch keeps the set of post ids it has already delivered in a key-value store. Every run fetches the top of the feed, subtracts what it has seen, delivers the difference, and writes the union back:
const store = await Actor.openKeyValueStore(`threads-watch-${name}`);
const seen = new Set((await store.getValue('seen')) || []);
const fresh = posts.filter((p) => !seen.has(p.id));
for (const p of posts) seen.add(p.id);
await Actor.pushData(fresh);
await store.setValue('seen', [...seen]);
Twelve lines, and the economics invert: you pay for the 60 new posts instead of the 18,000 repeats, and an hour when nothing was posted costs nothing at all. The same trick works for prices, job listings, and anything else you check more often than it changes.
Two details make it survive contact with reality. First, the very first run has nothing to compare against — deliver what is there and say so on the row, rather than returning an empty dataset that reads as a bug. Second, keep filtered-out posts in the seen set. A post you decided to ignore today must not come back tomorrow as breaking news.
The legal and practical boundaries
Public pages, read the way a visitor reads them, with no login and no account of yours at risk. Public data collection is generally lawful in the EU and the US, but personal data is regulated wherever it lands, and bios contain plenty of it. That is a question for your lawyer, not for your scraper.
Practically: no login means no private accounts, no follower lists, and no direct messages. It also means nothing you run can get an account of yours restricted, which is the trade most teams would take anyway.
If you would rather not build it
Both pieces exist as ready actors on the Apify platform:
- Threads Scraper — search, posts, replies, whole threads, profiles and account discovery, with the query-match filter and the null-instead-of-zero rule described above. $2 per 1,000 posts, errors free, no start fee.
- Threads Monitor — the delta mode as a product: name a watch, schedule it, receive only what appeared since the last check. A run that finds nothing new is free.
Both read the same public pages this article describes. The measurements above come from real runs, not from the documentation.
Top comments (0)