Get Threads profiles, posts and search

A handle returns the profile and the recent posts a logged-out viewer can see.

A Threads scraper, for a developer, is a small set of reads: the profile, the posts that profile shows publicly, and a keyword search. SocialScrape has those three as GET routes. All three are in beta. You do not create a Threads developer app to call them. You send a SocialScrape key in x-api-key. See authentication.

Profile#

GET /v1/threads/profile requires handle, the username with or without @. username is an alias. Errors are invalid_params and not_found.

The recorded request uses a public handle.

curl
curl -G "https://api.socialscrape.io/v1/threads/profile" \
  -H "x-api-key: $SOCIALSCRAPE_API_KEY" \
  --data-urlencode "handle=zuck"
Python
import os
import requests

params = {
    "handle": "zuck",
}
res = requests.get(
    "https://api.socialscrape.io/v1/threads/profile",
    headers={"x-api-key": os.environ["SOCIALSCRAPE_API_KEY"]},
    params=params,
)
body = res.json()

if body["ok"]:
    print(body["data"])
else:
    print(body["error"]["code"], body["error"]["message"])
TypeScript
const url = new URL("https://api.socialscrape.io/v1/threads/profile");
url.search = new URLSearchParams({
  "handle": "zuck"
}).toString();
const res = await fetch(url, {
  headers: { "x-api-key": process.env.SOCIALSCRAPE_API_KEY! },
});
const body = await res.json();

if (body.ok) {
  console.log(body.data);
} else {
  console.error(body.error.code, body.error.message);
}

data is a profile with platform threads. Fields you can rely on being present as keys, and null when Threads omits them: id, handle, name, bio, avatar_url, banner_url, url, verified, is_private, external_url, links, headline, location, category, created_at.

stats.followers and stats.posts are the public counts. stats.following is null in the recorded sample, which matches a profile page that does not show a following count. Do not assume every profile fills it. stats.likes and stats.views follow the same rule.

extra on the recorded profile has has_onboarded and topics. That is the extra block for this sample, not a promise that every account has topics filled.

Sample. The recording is shortened. Treat every integer in it as sample data.

Recent posts#

GET /v1/threads/profile/posts returns the most recent posts, newest first, about 15 per page. handle is required. cursor asks for the next page.

The limit is upstream. Threads shows logged-out visitors only the latest 20 to 30 posts of a profile, so paging runs out early. has_more goes false because that window ended, not because you have the account's whole history.

curl
curl -G "https://api.socialscrape.io/v1/threads/profile/posts" \
  -H "x-api-key: $SOCIALSCRAPE_API_KEY" \
  --data-urlencode "handle=zuck"
Python
import os
import requests

params = {
    "handle": "zuck",
}
res = requests.get(
    "https://api.socialscrape.io/v1/threads/profile/posts",
    headers={"x-api-key": os.environ["SOCIALSCRAPE_API_KEY"]},
    params=params,
)
body = res.json()

if body["ok"]:
    print(body["data"])
else:
    print(body["error"]["code"], body["error"]["message"])
TypeScript
const url = new URL("https://api.socialscrape.io/v1/threads/profile/posts");
url.search = new URLSearchParams({
  "handle": "zuck"
}).toString();
const res = await fetch(url, {
  headers: { "x-api-key": process.env.SOCIALSCRAPE_API_KEY! },
});
const body = await res.json();

if (body.ok) {
  console.log(body.data);
} else {
  console.error(body.error.code, body.error.message);
}

Each item is a post: id, url, text, published_at, author, media, stats. stats.likes, stats.comments and stats.shares are the counts when Threads sends them. stats.views and stats.saves are null when they are absent. extra on a feed post includes code, full_id, quotes, reply_control, reposts and shares_sent.

parent is the profile. Follow cursor while has_more is true, and stop when it is false. Each page is 1 credit. See pagination.

One post, with its top replies, is GET /v1/threads/post. The parameter is url, a threads.net/@user/post/... or threads.com post link. Replies are in extra.replies. The feed does not inline that tree. The recorded post URL in the reference is a public threads.com link, and that sample shows likes, comments and shares filled, with views null.

GET /v1/threads/search finds posts for a keyword. query is required. keyword and q are aliases. You get one page of about 15 to 20 results, and no further pages. The reference says there is no paging.

start_date and end_date (YYYY-MM-DD) filter that page by post date. Threads does not apply the dates itself. The endpoint filters the page it got. A date filter cannot surface posts that were not on the page.

The recorded search term is basketball.

curl
curl -G "https://api.socialscrape.io/v1/threads/search" \
  -H "x-api-key: $SOCIALSCRAPE_API_KEY" \
  --data-urlencode "query=basketball"
Python
import os
import requests

params = {
    "query": "basketball",
}
res = requests.get(
    "https://api.socialscrape.io/v1/threads/search",
    headers={"x-api-key": os.environ["SOCIALSCRAPE_API_KEY"]},
    params=params,
)
body = res.json()

if body["ok"]:
    print(body["data"])
else:
    print(body["error"]["code"], body["error"]["message"])
TypeScript
const url = new URL("https://api.socialscrape.io/v1/threads/search");
url.search = new URLSearchParams({
  "query": "basketball"
}).toString();
const res = await fetch(url, {
  headers: { "x-api-key": process.env.SOCIALSCRAPE_API_KEY! },
});
const body = await res.json();

if (body.ok) {
  console.log(body.data);
} else {
  console.error(body.error.code, body.error.message);
}

Items match the post shape above. extra on a search hit includes code, full_id, quotes, reply_control and reposts. There is no second page to request. If you need a different slice, change query.

Cost#

Each successful profile, page of posts, search or single post is 1 credit. All four are cacheable for 30 days. max_age_days makes a fresh-enough repeat free. Failures are free. The signup grant is 100 credits, once, with no card. Prices are on pricing. Rules are in credits.

Limits#

Plan on the logged-out window: roughly the latest 20 to 30 posts on a profile, delivered about 15 at a time. Search is a single page of about 15 to 20. Dates do not expand search. They only drop rows. Replies are a separate call per post URL.

This is public text and counts. It is not a private reply, a close-friends post, or a follower graph. not_found and invalid_params are final. rate_limited is not: wait retry-after. Free credits are 30 requests a minute per team. See rate limits and errors.

The API is in early access. These routes are beta.

Next#

Profile: Threads profile API, tutorial, reference. Posts: profile posts API and reference. Search: Threads search API and reference. The hub is the Threads API.

Using search as one source in a multi-platform listen is social listening. The Bluesky equivalent is Bluesky profiles and posts.

Questions

How far back do profile posts go?

Threads shows logged-out visitors only the latest 20 to 30 posts. Pages are about 15 posts, newest first, and paging runs out early even if the account has posted for longer.

Does search page?

No. Keyword search returns one page of about 15 to 20 posts. cursor does not fetch a second page.

Do start_date and end_date change what Threads searches?

Threads does not apply them. The endpoint filters the page it already received by post date, using YYYY-MM-DD.

Where are reply threads?

On GET /v1/threads/post, in extra.replies, for one post URL. The profile feed gives counts, not the reply tree.

Build on social data early.

Developers and agent builders get the first keys.

Get early access