Threads
Threads profiles, posts, replies, keyword search, and user search behind the unified schema
Meta's Threads behind the unified SocialCrawl envelope: profiles, a user's posts, one post's detail, the replies on a post, keyword search over public posts, and user search. Posts come back as the same canonical Post object you get from TikTok or Instagram, so one parser covers all three.
Base URL: /v1/threads/...
Threads search is a sampled window, not an index, and it is nondeterministic run to run. Running the identical query three times in a row on 09/08/2026 returned 20 posts each time and matched itself at only 0.693 Jaccard: 26 unique posts across the three runs. This is upstream behaviour and every Threads provider has it. A second run that returns a slightly different set is not data loss, and comparing two vendors on a single query measures the sampling, not the coverage.
Quickstart
1. Fetch a profile
curl "https://www.socialcrawl.dev/v1/threads/profile?handle=zuck" \
-H "x-api-key: YOUR_API_KEY"2. Fetch their content
curl "https://www.socialcrawl.dev/v1/threads/user/posts?handle=zuck" \
-H "x-api-key: YOUR_API_KEY"3. Read computed fields
When an endpoint supports a computed field and the required source inputs are present, the unified response includes that optional field. Depending on the endpoint, optional fields can include engagement_rate, language, content_category, and estimated_reach. See Computed fields for formulas, clamping rules, and null semantics. Where an endpoint supports them, list rows also carry judged labels (computed.labels, computed.relevance) by default at no extra credits; see Labels.
Accounts and posts
| Endpoint | Credits | What it returns | Key parameters |
|---|---|---|---|
GET /v1/threads/profile | 1 | Followers, bio, avatar, verification, profile URL, and the bio link | handle |
GET /v1/threads/user/posts | 1 to 165 | Roughly 15 recent posts, as one fixed window; include=engagement adds the view count and display name to each of them | handle, trim, include, limit (1-50) |
GET /v1/threads/post | 1 | One post's full detail object | url, trim |
Handles are passed without the @. The external website a user sets in their bio comes back at author.ext.bio_link, null when they have not set one. author.url is the account's Threads permalink, built from the handle on both profile and search/users.
Four Author leaves are always null on Threads, because Threads does not publish them: following, posts_count, likes_count and joined_at. Treat them as structurally absent rather than missing data. The follower count and bio are a different case: they are absent from the account-search index but present on profile, so search/users fills them on request (see Filling the numbers a list leaves out).
user/posts has no cursor, because the upstream offers none. It has a depth lever instead: leave limit off, or send 15 or less, and the call is the bundled window at exactly 1 credit, while limit above 15 collects up to 50 posts of the same feed from a second source, metered at 3 credits per post returned. Those deeper rows already carry the view count and display name that include=engagement fills on the bundled window. For older posts still, run search with the handle or a distinctive phrase and a date window, or reach for /v1/search/everywhere, which includes Threads as a source.
Replies
| Endpoint | Credits | What it returns | Key parameters |
|---|---|---|---|
GET /v1/threads/post/comments | 1 to 10 | The reply window bundled with the post, typically about 20 replies | url, trim, limit (1-50) |
Each reply carries text, like count, direct reply count, author handle, display name, avatar, verification, pinned flag, and timestamp.
This is the same upstream call as GET /v1/threads/post. One request returns the post and the replies bundled together, and the two endpoints just pick different halves. A post and its replies therefore cost one credit each, and there is no way to save by combining them. That bundling has three consequences worth knowing before you build on it:
- It is one window with no cursor, so there is no way to page deeper into a long conversation. Roughly 20 replies is what exists.
comment.urlis constructed from the reply's shortcode and its author handle, because the upstream sends no permalink on reply items.comment.post_idandcomment.parent_idarenullfor the same reason: the upstream surfaces neither id on a reply. If you need to attach a reply to its parent post, carry the post url through yourself.
curl "https://www.socialcrawl.dev/v1/threads/post/comments?url=https://www.threads.com/@zuck/post/DZpPDXbCeTt" \
-H "x-api-key: YOUR_API_KEY"Search
| Endpoint | Credits | What it returns | Key parameters |
|---|---|---|---|
GET /v1/threads/search | 1 to 26 | Public posts matching a keyword; include=engagement adds the view count and pinned flag to the first 20 | query, limit (1-100), start_date, end_date, cursor, expand, include, label, relevance, judgments |
GET /v1/threads/search/users | 1 to 13 | Accounts matching a term, with display name, avatar and verification; include=profile adds the follower count, bio, private flag and bio link | query, include, limit (1-12) |
search returns a single result window, typically 15 to 20 posts. Threads gives the upstream no cursor at all; its only paging lever is the date filter, so the endpoint synthesises pagination for you. There are two ways to go deeper:
-
Send the cursor back. Pass
pagination.next_cursorfrom the previous response ascursorto get the next, older window. One window, 1 credit. -
Ask for a count and let the API walk. Set
limitand the API re-queries shrinking date windows server-side, de-duplicates by post id, and returns at least that many unique posts in one call. It bills 1 credit per window consumed, about 15 to 20 posts each, and refunds the unused window budget. Alimit=100call that runs dry after three windows costs 3 credits, not 7.limitis a collection target, not a page size. The walk stops at the first whole window that reaches your number, solimit=40typically returns 50 to 60 posts, and nothing you were billed for is trimmed away. Slice your own copy if you need exactly N. Values outside 1 to 100 are rejected with a free400rather than clamped.
start_date and end_date are both YYYY-MM-DD and inclusive. The walk starts at end_date and works backwards, and never crosses start_date, which makes the pair the right way to page a fixed period without runaway cost. A relaxed query obeys the same bounds. Anything that is not YYYY-MM-DD, and any window whose start is after its end, is a free 400: Threads treats the date pair as a ranking hint rather than a filter, so the window is enforced on our side and a post we cannot date is treated as outside it.
The first call below is the cheapest possible probe, one window. The second collects up to 60 unique posts from a bounded period in a single call.
curl "https://www.socialcrawl.dev/v1/threads/search?query=artificial%20intelligence" \
-H "x-api-key: YOUR_API_KEY"
curl "https://www.socialcrawl.dev/v1/threads/search?query=artificial%20intelligence&limit=60&start_date=2026-07-01&end_date=2026-07-31" \
-H "x-api-key: YOUR_API_KEY"If you need a stable set, collect with limit over a bounded date window and de-duplicate on post id your side.
Long queries are handled for you
Threads matches a search against the topic tags it already knows rather than scoring individual terms. A phrase it does not recognise therefore returns nothing at all instead of fewer results, and the longer the phrase, the less likely it is to be a tag. This is not a keyword limit: machine learning engineer and new york city both return a full window, while the best coffee returns zero.
When a query comes back empty, the API relaxes it into its adjacent word pairs, runs those searches in parallel, and returns one merged, de-duplicated list ordered by how many of your terms each post contains. Measured on 31/08/2026, best coffee machine for a small office went from 0 posts to 75, and cari vendor mesin kopi jakarta from 0 to 56.
curl "https://www.socialcrawl.dev/v1/threads/search?query=best%20coffee%20machine%20for%20a%20small%20office" \
-H "x-api-key: YOUR_API_KEY"Three things to plan around:
- You are billed 1 credit for each relaxed search that returned posts, and nothing for the ones that did not. A query that already worked is never relaxed, so it still costs exactly 1 credit. The worst case is 4 relaxed searches on top of the original.
- A relaxed response is a single page with no
pagination.next_cursor, because the cursor would resume the exact phrase that just returned nothing. For the same reason, a request that carries a cursor is never relaxed, so the last hop of a pagination loop is the honest end of the walk rather than a surprise page. - Relaxation respects your date window. Posts outside
start_date/end_dateare dropped from every sub-query, and a sub-query left with nothing inside the window is not billed. data._warningsnames every sub-query that ran, so you can always see what was actually searched.
Send expand=false to switch this off and search the exact phrase only, which restores the older behaviour of an empty result at 0 credits.
Filling the numbers a list leaves out
Three Threads lists are thin by construction: the account-search index carries no follower count or bio, and neither post list carries a view count. Each of those numbers is on a sibling endpoint, so instead of calling it once per row yourself, add include and the API does the join inside the same call.
| Call | Token | What lands on each row | Cost |
|---|---|---|---|
search/users | include=profile | author.followers, author.bio, author.private, author.ext.bio_link | 1 per account filled, 12 at most |
user/posts | include=engagement | engagement.views, post.author.display_name, and the computed block that divides by views | 1 per post filled, 15 at most |
search | include=engagement | engagement.views, post.flags.pinned on the first 20 posts | 1 per post whose view count landed, 20 at most |
curl "https://www.socialcrawl.dev/v1/threads/search/users?query=tech&include=profile" \
-H "x-api-key: YOUR_API_KEY"Four things to plan around:
- You pay for rows that come back filled, and nothing else. A row the lookup could not fill is refunded and named in
data._warnings; a row we fetched recently is served from cache for nothing.limitonsearch/userscaps the rows and the credits together, solimit=5&include=profilecosts at most 6. - It adds seconds, because it is doing the calls you would have made. Measured on 11/09/2026: about 9 seconds for 11 accounts, 10 for 15 posts, 10 for 17 search results. Each lookup is bounded, so a slow one is dropped and refunded instead of holding the response.
data.hydrationexplains the bill. Every response that usedincludereports the rows on the page and how many were looked up, filled, served from cache and left unfilled, alongside the credits held and kept.- On
searchit is one window.include=engagementjoins the first 20 posts and is not accepted withlimitabove 20; page with the cursor instead, 20 posts a call. - A brand-new post has no view count yet, and it is free. Threads publishes a view count only once it has one, so a post that is minutes old comes back without it, on this endpoint and on
GET /v1/threads/postalike. That row still gets its pinned flag, and it costs nothing: onsearchyou are billed only for posts whose view count (or missing display name) actually landed. Measured 12/09/2026 onartificial intelligence, a page of very fresh posts: 11 of 19 filled and billed, the other 8 filled their pinned flag for free.
Quote posts
A Threads post that quotes another usually has no caption or media of its own, so a naive reader sees an empty post. The quoted content is returned whole at post.ext.quoted_post as { id, url, author, text, media_urls, thumbnail_url }. It belongs to a different account, so it stays there rather than being merged into content, and post.content is always this post's own content. The field is absent on posts that are not quote posts, and it is present on all three post surfaces: post, user/posts and search.
Topic tags
Posts filed under a Threads topic tag carry the tag name at post.ext.topic_tag, with values that look like bali or dokter gigi, and its stable id at post.ext.topic_tag_id. Both fields are present on search, user/posts, and post.
Two things to plan around. Both fields are absent on untagged posts, which are the majority, so treat them as optional and never assume they exist. And Threads exposes no tags-only search mode upstream, so there is no way to ask for "every post under bali". The supported pattern is to run a keyword search and group or filter the results by topic_tag client-side.
All endpoints
6 endpoints available.
| Endpoint | Path | Credit Tier |
|---|---|---|
| Get Threads post details | /v1/threads/post | standard (1cr) |
| Get comments on a Threads post | /v1/threads/post/comments | standard (1-10cr)metered |
| Get Threads user profile | /v1/threads/profile | standard (1cr) |
| Search Threads posts | /v1/threads/search | standard (1-34cr)metered |
| Search Threads users | /v1/threads/search/users | standard (1-13cr)metered |
| List Threads user posts | /v1/threads/user/posts | standard (1-165cr)metered |
Platform notes
A Threads profile only exists if the account opted in. Many well-known Instagram usernames have no Threads account, so a 404 is a genuine "no such profile" rather than an error.
Nothing here pages except search. user/posts, post/comments and search/users each return one window with no cursor to resume from. The incumbent source exposes no depth lever at all on user/posts: cursor, next_cursor, page, offset, limit, amount, count and max were each probed on 04/09/2026 and every one returned the same 15 rows. limit above that boundary therefore moves the call to a second source (up to 50 posts on user/posts, up to 50 replies on post/comments), and the response is still a single page.
Threads publishes engagement.views on a single post's page and not on any list surface, so user/posts and search return null for it on a plain call. Add include=engagement and each row is looked up on GET /v1/threads/post inside the same call, at 1 credit per row filled. Fetching the one post you need still works and still costs 1 credit.
Reshares split three ways. engagement.shares is the plain repost count. A repost that added the resharer's own commentary is counted at post.ext.quote_count, and the boosted figure Threads shows in-app is at post.ext.reshare_count. Add them yourself if you want one number; we do not, because the three mean different things.
post.flags.pinned says whether the author pinned the post to their profile. It is populated on user/posts and post. On search the upstream omits the field rather than reporting false, so it is null on a plain call and filled on the first 20 rows when you send include=engagement.
Media and avatar URLs are signed Instagram CDN links carrying oh= and oe= expiry parameters. They work for a matter of hours, not indefinitely. Download what you need to keep, and re-fetch the post rather than storing the URL.
Carousels and video land in the same place. Carousel posts return every slide as an array in post.content.media_urls; video posts return a playable video URL there, with the cover frame in post.content.thumbnail_url.
trim=true costs the same and removes more than whitespace. It works on user/posts, post, post/comments and search, and the trimmed record the upstream returns carries only the id, text, shortcode, like count, timestamp and author. Everything else comes back null: media, reply count, share count, view count, topic tag, pinned flag and quoted post. It is the right flag when you want text and likes at a smaller payload, and the wrong one any other time, because it costs the same 1 credit either way.
Labels
These lists label their rows by default, at no extra credit. On /v1/threads/search, each row carries sponsored, intent and niche. /v1/threads/search also scores each row's relevance to your query on computed.relevance, without dropping or reordering anything; add relevance=filter to drop the rows that are about something else, still free. Send judgments=off for the page without them. See Labels for the field shapes, rows still pending, and the labels you can add with label=.
Next steps
Pagination
Cursor and page walks, and what has_more actually means.
Credits
What each tier costs and when a call is refunded.
API reference
Every parameter and response field, endpoint by endpoint.
The Meta network Threads accounts are attached to.
Bluesky
The other text network, same canonical Post object.
