The Mine Works
Threads Has No Public API in 2026: Here Is How to Get Post and Profile Data Anyway
← All posts
tutorial June 22, 2026 · 16 min read Updated October 9, 2026

Threads Has No Public API in 2026: Here Is How to Get Post and Profile Data Anyway

Every field you can collect from Threads without an API: post text, engagement counts, media, profiles. No login required, from $1 per 1,000 posts, plus a comparison of the scrapers that do it.

Try the scraper

The actor referenced in this article. Pay only for results delivered.

View the scraper →

When Meta launched Threads in July 2023, it grew to 100 million users in under a week. That is a significant audience for brand monitoring, competitive research, and social listening. There is one problem: Meta has not released a public Threads API.

The Instagram Graph API does not cover Threads. The oEmbed endpoint covers only individual public posts you already have URLs for, which is not useful for discovery. The Meta Developer platform has no Threads-specific permission scope. If you want structured Threads data at scale, you need a different approach.

TL;DR: Threads has no public API as of mid-2026. Profile data and post content are publicly accessible via the web interface. A scraper navigates the public profile pages, extracts structured data from the embedded JSON, and paginates through posts without requiring authentication. Fields include post text, like counts, reply counts, timestamps, and profile metadata.

Why Meta Has Not Released a Threads API

The short answer is product maturity. Threads launched fast, built on Instagram’s infrastructure, and the engineering team has been iterating on core features rather than developer tooling. Meta’s public statement has been that API access is planned but not yet available.

There is also a regulatory dimension. The EU’s Digital Markets Act requires large platforms to open APIs for interoperability, and Meta is navigating how Threads fits into that framework alongside its existing platforms. This is a meaningful constraint. Until the regulatory picture settles, a formal API is less likely.

The ActivityPub integration (Threads supports the fediverse protocol) gives you some signal for public profiles that opt in, but it is incomplete and not useful for bulk data collection.

Meta does run a Threads API for accounts that want to publish and manage their own posts. It works through per-account access tokens and app review, and it is not a way to read other accounts’ posts at volume, which is what monitoring and research need.

What Is Publicly Accessible

Threads public profiles are accessible without authentication. For any public account, you can view:

  • Profile information: username, display name, bio, follower count, following count, verification status
  • Posts: text content, post type (thread, reply, repost), timestamps, like count, reply count, repost count
  • Media attachments: image URLs and video thumbnails (not the video files themselves)
  • Reply threads: the chain of replies in a thread conversation

What is not accessible without a logged-in session:

  • Private accounts (obviously)
  • Direct messages
  • Notifications
  • Draft content
  • Historical data beyond what the web UI surfaces (a logged-out visitor sees only the latest handful of posts on a profile)
  • Impression and reach counts, which only the post author sees
  • Deep reply trees (you get the direct replies to a post, not every nested level)
  • A real-time stream (there are no webhooks, so you poll on a schedule)

The data that is accessible covers most brand monitoring and competitive intelligence use cases. If you want to track what a brand is saying, how their audience is engaging, and how their follower count is trending, the public profile data is sufficient.

How the Scraper Navigates Without Auth

The Threads web interface is a React application that loads profile and post data from internal GraphQL endpoints. These are not documented, not versioned for external use, and not stable across releases. However, the page HTML includes a __NEXT_DATA__ JSON blob or a script tag with structured data that the React app uses to render the initial page state.

The scraper works by:

  1. Fetching the public profile URL (https://www.threads.net/@username)
  2. Extracting the embedded JSON from the page source
  3. Parsing the profile metadata and the initial post batch
  4. Following the pagination cursor to load additional posts

The scraper does not log in. It does not touch the internal GraphQL endpoints directly (which are more fragile). Instead it works from the public HTML in the same way a search engine crawler would. This approach is slower than an authenticated API call but is stable against minor API changes because the page HTML rendering is separate from the internal endpoint structure.

Rate limiting is handled by spacing requests and respecting the server’s response behavior. Threads will slow-down or temporarily block IPs that make requests at machine speed without any gaps. Good practice means building in delays, rotating requests if you are doing bulk pulls, and not hammering a single profile repeatedly.

Why Not Just Log In?

Most DIY Threads scripts you will find take the logged-in route instead. They copy the sessionid and csrftoken cookies from a browser signed in to Instagram, then call Threads’ internal GraphQL endpoint (/api/graphql) with a doc_id for each operation. A more stubborn version imitates the mobile app, sending the app’s user agent, an X-IG-App-ID header and a fixed device ID.

Both work for a while, and both need constant upkeep:

  • The doc_id hashes change with app releases, often every few weeks, so hard-coded IDs break without warning.
  • Session cookies expire, typically within about 90 days.
  • Meta fingerprints devices and watches request timing across Instagram and Threads. A device ID that makes requests at machine speed gets flagged, and so does one that rotates too often.
  • The account itself carries the risk. A flagged account can be rate limited or banned, so never use a personal one.

The logged-out route avoids all of that. The trade-off is that you only get what a logged-out visitor sees, which is the list under What Is Publicly Accessible.

Fields Returned Per Post

Here is the data structure for a single Threads post as returned by the scraper:

{
  "post_id": "3391234567890123456",
  "code": "abc123",
  "url": "https://www.threads.com/@threadsindia/post/abc123",
  "username": "threadsindia",
  "user_full_name": "Threads India",
  "user_verified": false,
  "posted_at": "2026-06-18T10:23:14.000Z",
  "text": "The next generation of conversation starts here.",
  "like_count": 1842,
  "reply_count": 203,
  "repost_count": 94,
  "quote_count": 11,
  "has_media": true,
  "media_type": "image",
  "media_urls": [
    "https://scontent-bom1-1.cdninstagram.com/v/t51.2885-15/..."
  ],
  "is_reply": false,
  "parent_post_id": null,
  "is_repost": false,
  "original_post_id": null,
  "hashtags": [],
  "mentions": [],
  "urls": [],
  "user_pic_url": "https://scontent-bom1-1.cdninstagram.com/...",
  "posted_at_human": "Thu, 18 Jun 2026 10:23:14 GMT",
  "scraped_at": "2026-06-18T11:02:50.117Z"
}

Threads field names and what the scraper calls them

Threads’ own web payload uses different names from the ones in the dataset above, and those internal names are what you will find if you read the network tab. The scraper flattens text_post_app_info and renames the counters, so the mapping is worth having side by side.

Name in the Threads payloadField in the datasetNotes
pk or idpost_idNumeric string, stable per post
codecodeShort code used in the public post URL
caption.texttextPlain text, no entity markup
taken_atposted_atUnix seconds in, ISO 8601 out
like_countlike_countUnchanged
text_post_app_info.direct_reply_countreply_countDirect replies only, not the whole thread
text_post_app_info.repost_countrepost_countUnchanged
text_post_app_info.quote_countquote_countCount of quotes, not the quoted post
text_post_app_info.reply_to_author.pkparent_post_idNull when the post is not a reply
text_post_app_info.share_info.reposted_post.pkoriginal_post_idNull when the post is not a repost

Two names people look for are not in the payload at all. There is no quote_post_id and no reply_to_id on a public Threads post: you get quote_count (how many times it was quoted) and reply_to_author.pk (who it answered), which the dataset exposes as parent_post_id. To walk a thread you follow parent_post_id back up, one call per level.

Profile-level fields come attached to each post rather than as a separate record:

{
  "username": "threadsindia",
  "user_full_name": "Threads India",
  "user_verified": false,
  "user_pic_url": "https://scontent-bom1-1.cdninstagram.com/..."
}

There is no follower count, following count or bio in there, and that is not an omission on our side. Threads only serves those numbers to a logged-in session, so no scraper can return them without an account, and anything that claims to is either logged in or guessing.

Python Example: Fetch a Profile

The managed Threads scraper handles the extraction and pagination. You call it through the Apify API:

import requests
import time

APIFY_TOKEN = "your_apify_token"

def fetch_threads_profile(username):
    """Fetch profile metadata and recent posts for a Threads username."""
    
    # Start the actor run
    run_url = f"https://api.apify.com/v2/acts/themineworks~threads-scraper/runs"
    
    payload = {
        "mode": "profile",
        "profileUsernames": [username],
        "maxPosts": 50,
        "includeReplies": False,
    }
    
    resp = requests.post(
        run_url,
        json=payload,
        params={"token": APIFY_TOKEN}
    )
    resp.raise_for_status()
    run_id = resp.json()["data"]["id"]
    
    # Wait for the run to finish
    status_url = f"https://api.apify.com/v2/actor-runs/{run_id}"
    while True:
        status_resp = requests.get(status_url, params={"token": APIFY_TOKEN})
        status = status_resp.json()["data"]["status"]
        if status in ("SUCCEEDED", "FAILED", "TIMED-OUT", "ABORTED"):
            break
        time.sleep(3)
    
    if status != "SUCCEEDED":
        raise RuntimeError(f"Scraper run ended with status: {status}")
    
    # Retrieve results
    dataset_id = status_resp.json()["data"]["defaultDatasetId"]
    results_url = f"https://api.apify.com/v2/datasets/{dataset_id}/items"
    results = requests.get(results_url, params={"token": APIFY_TOKEN}).json()
    
    # the last row of every run is an unbilled info record, not a post
    return [r for r in results if not r.get("_type")]

profile_data = fetch_threads_profile("zuck")
print(f"Fetched {len(profile_data)} posts")
for post in profile_data[:3]:
    print(f"[{post['posted_at']}] {post['like_count']} likes: {post['text'][:80]}")

Python Example: Pull Posts with Pagination

To cover several accounts, pass them all in profileUsernames and page through the dataset yourself. maxPosts caps the whole run (up to 500), and a logged-out profile only shows its latest 4 or 5 posts, so adding accounts is how you get a bigger total:

def bulk_monitor_accounts(usernames, posts_per_account=5):
    """Pull posts for a list of Threads accounts and return flat list."""
    
    run_url = f"https://api.apify.com/v2/acts/themineworks~threads-scraper/runs"
    
    payload = {
        "mode": "profile",
        "profileUsernames": usernames,
        "maxPosts": min(posts_per_account * len(usernames), 500),  # one cap for the whole run
        "includeReplies": False,
    }
    
    resp = requests.post(
        run_url,
        json=payload,
        params={"token": APIFY_TOKEN}
    )
    resp.raise_for_status()
    run_id = resp.json()["data"]["id"]
    
    # Poll until done
    status_url = f"https://api.apify.com/v2/actor-runs/{run_id}"
    while True:
        r = requests.get(status_url, params={"token": APIFY_TOKEN}).json()
        if r["data"]["status"] in ("SUCCEEDED", "FAILED"):
            break
        time.sleep(5)
    
    dataset_id = r["data"]["defaultDatasetId"]
    items_url = f"https://api.apify.com/v2/datasets/{dataset_id}/items"
    
    all_posts = []
    offset = 0
    limit = 100
    
    while True:
        page = requests.get(
            items_url,
            params={"token": APIFY_TOKEN, "offset": offset, "limit": limit}
        ).json()
        
        if not page:
            break
        
        all_posts.extend(p for p in page if not p.get("_type"))  # skip the info row
        
        if len(page) < limit:
            break
        
        offset += limit
    
    return all_posts

# Monitor a competitor set
competitor_accounts = ["brandaccount1", "brandaccount2", "brandaccount3"]
posts = bulk_monitor_accounts(competitor_accounts, posts_per_account=5)

# Basic engagement analysis
import statistics
like_counts = [p["like_count"] for p in posts if "like_count" in p]
print(f"Total posts collected: {len(posts)}")
print(f"Median likes: {statistics.median(like_counts):.0f}")
print(f"Max likes: {max(like_counts)}")

Search, Hashtag and Single-Post Modes

Profile mode is one of four. The same actor also takes a keyword (search), a tag (hashtag) or a list of post URLs (post). Search and hashtag runs accept resultType of recent or top.

from apify_client import ApifyClient

client = ApifyClient(APIFY_TOKEN)

run = client.actor("themineworks/threads-scraper").call(run_input={
    "mode": "search",
    "searchQuery": "your brand",
    "resultType": "recent",
    "maxPosts": 200,
    "monitorMode": True,  # on scheduled re-runs, return only posts not delivered before
})

posts = [p for p in client.dataset(run["defaultDatasetId"]).iterate_items() if not p.get("_type")]
for p in posts[:5]:
    print(p["username"], p["like_count"], p["reply_count"], p["url"])

For hashtags, swap in "mode": "hashtag", "hashtag": "ai" (no #). For single posts, use "mode": "post", "postUrls": [...], which returns each post with its quoted or parent context.

How many posts to expect

Threads does not serve every surface the same way to a logged-out visitor, so the yield depends on the mode:

ModeTypical yield per run
Searchtens to hundreds of posts
Hashtagtens of posts
Postone record per URL you pass
Profileabout 4 to 5 recent posts per username

The profile figure is Threads’ limit, not the scraper’s. A logged-out profile shows its latest handful of posts and then repeats them, so raising maxPosts does not get you more. If you want many posts about a person or brand rather than by them, run a search on their name or handle and filter on username.

A single logged-out search feed stops at about 20 posts. The scraper gets past that by combining the recent and top feeds, the matching tag feed and repeated recency windows, then removing duplicates. Search and tag pages sometimes come back with no post data at all, which is a condition on Threads’ side. The run stops after one request when that happens and charges nothing, so retry after a short wait.

monitorMode is what makes a scheduled watch affordable. The scraper remembers which posts it already delivered for that input and bills only for new ones.

Use Cases

Brand monitoring. Track mentions of your brand name or product across public Threads accounts. Threads has no keyword search API, but search mode reads the same results a logged-out visitor gets, so you can watch a brand name or product term directly. Run it on a schedule with monitorMode on, and sort what comes back by likes and replies to see which mentions carry weight.

Social listening for competitive analysis. Pull posts from competitor accounts and analyze engagement patterns. Which content types (text-only vs. image) drive more replies? What posting cadence correlates with higher engagement? This analysis is straightforward once you have structured post data.

Content research for brand accounts. If you run a Threads presence, looking at what posts from similar accounts perform well gives you real data for content planning. Pulling a few hundred posts on your category’s keywords takes a few minutes. Doing it by hand takes hours.

Influencer vetting. Before a paid partnership, pull actual engagement numbers from a creator’s Threads account. Reported follower counts are easy to inflate. The ratio of follower count to actual reply and repost counts on recent posts is a useful signal for authentic reach. Also look at how often they post, how much is original versus reposted, and how they have handled other brands’ products in the past. Searching their handle surfaces the conversation around them as well as their own posts.

Newsroom and agency trend tracking. Media teams watch search and hashtag results to see what is gaining traction on Threads before it reaches bigger platforms. Threads is also where many brands now post informally, so a weekly pull can surface announcements that never get cross-posted to Instagram or Facebook.

Academic research. Threads is a significant venue for public discourse. Researchers studying social platform dynamics, misinformation spread, or public communication patterns need structured data. The scraper produces data in the format academic pipelines expect.

Social Listening With Claude

Once the posts are structured, a language model can do the reading. That matters on Threads because short posts trip up keyword classifiers (“I hate that I love this” reads as negative to a word list), while a model picks up the tone. The helpers below reuse client from the search example.

import json
import anthropic

claude = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY

def search_threads(query, max_posts=100, result_type="recent"):
    run = client.actor("themineworks/threads-scraper").call(run_input={
        "mode": "search", "searchQuery": query,
        "resultType": result_type, "maxPosts": max_posts,
    })
    return [p for p in client.dataset(run["defaultDatasetId"]).iterate_items() if not p.get("_type")]

def ask_claude(prompt, model="claude-opus-5", max_tokens=16000):
    msg = claude.messages.create(model=model, max_tokens=max_tokens,
                                 messages=[{"role": "user", "content": prompt}])
    return next(b.text for b in msg.content if b.type == "text")  # skip any thinking block

def engagement(p):
    return p["like_count"] + 3 * p["reply_count"]  # a reply takes more effort than a like

Brand mention triage

Classify mentions in batches of ten with a small, fast model, then pull out the ones that need a person to answer:

def classify_mentions(brand, posts, batch_size=10):
    labelled = []
    for i in range(0, len(posts), batch_size):
        batch = posts[i:i + batch_size]
        listing = "\n\n".join(
            f"POST {n + 1} (@{p['username']}, {p['like_count']} likes, {p['reply_count']} replies):\n{p['text'][:500]}"
            for n, p in enumerate(batch)
        )
        prompt = (
            f"These Threads posts mention {brand}. For each post, in order, return an object with "
            "sentiment (positive, negative, neutral or mixed), intent (complaint, praise, question, "
            "comparison or mention), urgency (1-5) and needs_reply (true or false). "
            f"Return only a JSON array of {len(batch)} objects.\n\n{listing}"
        )
        text = ask_claude(prompt, model="claude-haiku-4-5", max_tokens=2000)
        try:
            labels = json.loads(text.strip().removeprefix("```json").removesuffix("```"))
        except json.JSONDecodeError:
            labels = [{}] * len(batch)
        labelled += [{**p, **label} for p, label in zip(batch, labels)]
    return labelled

mentions = classify_mentions("YourBrand", search_threads("YourBrand"))
for m in mentions:
    if m.get("needs_reply") and m.get("urgency", 0) >= 4:
        print(m["urgency"], m["url"], m["text"][:100])

Take the most engaged posts across a few category keywords and ask for the arguments people are having, not topic labels:

def trending_conversations(keywords, niche):
    posts = {p["post_id"]: p for k in keywords[:5] for p in search_threads(k, 50, "top")}
    top = sorted(posts.values(), key=engagement, reverse=True)[:40]
    digest = "\n".join(
        f"- [{p['like_count']} likes, {p['reply_count']} replies] @{p['username']}: {p['text'][:200]}"
        for p in top
    )
    return ask_claude(
        f"You follow the {niche} space. From these high-engagement Threads posts, name 4 to 6 "
        "distinct conversations. For each, say what the argument is, why it is getting traction "
        "now, how people feel about it, and what a brand could usefully add. Name the actual "
        f"narratives, not broad categories.\n\n{digest}"
    )

The same data also gives you share of voice. Search your category keywords, count how many posts mention each brand’s name variants, and hand the percentages plus a few sample posts per brand to Claude for a short read on who owns the conversation and whether those mentions are positive. With posts from competitor accounts added, the same approach produces draft posts in your own voice, based on what is already working in your niche.

A sensible cadence is the mention triage daily and the trend and share-of-voice reports weekly. Scraper cost scales with the number of posts delivered (see pricing).

Rate Limits and Responsible Scraping

There is no official rate limit because there is no official API. The practical limits come from Threads’ anti-bot infrastructure, which is similar to Instagram’s (they share infrastructure).

Sensible limits for sustained use: 200 to 500 posts per minute across all requests, with randomized delays between profile fetches (2 to 5 seconds). For bulk historical pulls of a single account, going faster than 100 posts per 30 seconds is where you start seeing increased error rates.

The scraper is built with these limits in mind. It does not exceed what a fast human user would do, which is the threshold that keeps it stable over time.

If you are building a scheduled monitoring workflow, running the scraper once every few hours over a watchlist of accounts is stable indefinitely. Running it at maximum speed continuously against the same accounts will eventually result in IP-level throttling.

Meta’s terms of service allow personal, non-commercial use of public Threads data. Commercial use of scraped data is a grey area that depends on what you are doing with it. This is worth reviewing against your specific use case before building production workflows around it.

Public posts can still contain personal data under the GDPR and US state privacy laws. Collect only what your use case needs, do not keep more than that, and leave private accounts alone.

For how the Threads scrapers on the Apify Store compare on price and reliability, see Best Threads scrapers compared. For Threads against X as a data source, see Threads vs Twitter/X data.

Related Actor

Explore the scraper referenced in this article: inputs, outputs, and pricing, then run it on Apify.

Apify Store

Find a ready-made scraper for your job

Apify has over 80,000 scrapers and automations, ours included. Start free with $5 of platform credit every month.