# Threads Scraper - Posts, Profiles, Search, Hashtags (`s-r/threads-scraper`) Actor

Scrape Threads posts with their like, reply, repost and quote counts. Works four ways: a user's own posts with their follower count, one post plus all the replies under it, a keyword search, or a hashtag. Returns text, timestamps, media URLs, link previews and detected language.

- **URL**: https://apify.com/s-r/threads-scraper.md
- **Developed by:** [SR](https://apify.com/s-r) (community)
- **Categories:** Social media, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Threads Scraper

Pull posts off Threads with the numbers that matter: likes, replies, reposts
and quotes, plus the text, the timestamp, the media and the link preview. No
login, no cookies to supply, no token to keep alive.

It works four ways, and you can mix all four in a single run.

### The four modes

**A profile.** Give it `zuck` or `@zuck` and you get that account's recent
posts along with the profile itself: follower count, full name, biography and
verification status. Roughly ten posts per profile, which is what Threads
renders on a profile page.

**One post and its replies.** Paste a post URL and you get the post plus every
reply rendered underneath it, usually around thirty rows. This is the mode for
reading a conversation rather than a feed, and each reply carries its own
engagement counts so you can see which answer actually landed.

**A keyword search.** Give it a phrase such as `web scraping` and you get
around twenty matching posts from across Threads.

**A hashtag.** Give it `#ai` and you get the posts filed under that tag.

Leave the mode on **Auto** and each entry decides for itself. A bare word is
tried as a handle first and then as a search, so `apify` returns posts about
Apify rather than an error when no such account exists. Every row records which
reading produced it in `source_mode`, so you always know what you are looking
at.

### What each post gives you

- `text`, `posted_at` (ISO 8601) and `posted_timestamp`
- `like_count`, `reply_count`, `repost_count`, `quote_count`, `reshare_count`
- `username`, `user_id`, `is_verified`
- `media_kind` (`text`, `image`, `carousel` or `video`), plus `image_urls` and
  `video_urls`. Images come back at the largest published resolution rather
  than the thumbnail, because the thumbnail is rarely what anyone wants.
- `link_url` and `link_title` when the post carries a link preview card
- `is_reply`, so you can separate original posts from replies in one filter
- `language`, as detected by Threads itself
- `is_paid_partnership`, which is the disclosure flag on sponsored posts
- `url`, a direct link back to the post

Profile runs additionally carry `profile_follower_count` on every row, and the
run summary holds the full profile records.

### What the run summary tells you

Targets requested, targets that produced posts, total posts, profiles found,
total likes across everything returned, how many posts carried media, and how
many were replies. Enough to sanity-check a run at a glance without opening the
dataset.

### The failure mode this was built to avoid

Threads answers **HTTP 200 with a full-size page** for a handle that does not
exist. The response is around 315 KB, which is the same order of magnitude as a
real profile. Neither the status code nor the size can tell the two apart.

An actor that trusted the status code would report every typo'd handle as a
real account that simply has not posted, and you would never know. So liveness
is decided on the profile data itself: a real account carries a follower count,
and a page without one is reported as an error naming the handle rather than as
an empty success.

This matters more than it sounds. Silent wrong answers are worse than loud
failures, because a row that says "0 posts" looks like data.

### Input

A list of targets, and optionally:

- **Mode** — force every entry to be read the same way, instead of Auto
- **Maximum posts per target** — an upper bound per entry
- **Maximum targets** — how many entries to process in one run
- **Targets in parallel** — how many to fetch at once
- **Retries** — attempts with backoff before an entry is reported failed
- **Request from country** — a two-letter code, when you want the page as it is
  served in a particular region

### Notes on scale and honesty

Each target costs exactly one page fetch, and a run of fifty targets at the
default concurrency finishes in seconds. There is no crawling, no pagination
loop and no per-post follow-up request, so the cost is predictable.

What you get is what Threads renders on the page. That is around ten posts for
a profile rather than the full history, and around twenty for a search. If you
need a longer history, run a profile repeatedly over time and keep the rows:
`post_id` is stable, so deduplicating across runs is trivial. The actor does
not pretend to page deeper than the site actually serves, because an actor that
quietly returned fewer rows than it implied would be the same class of problem
as the one described above.

Counts are read at the moment of the fetch. Likes and replies on a fresh post
move quickly, so two runs minutes apart will legitimately disagree, and that is
the data rather than an error.

Deleted posts and private accounts return nothing, and are reported as errors
naming the target rather than dropped from the output.

# Actor input Schema

## `targets` (type: `array`):

What to scrape. Mix freely: a handle such as zuck or @zuck returns that profile's posts, a #hashtag returns posts under it, a plain phrase searches, and a full Threads URL to a profile or a post works as pasted. A post URL returns the post plus the replies under it.

## `mode` (type: `string`):

Leave on Auto to let each target decide for itself. Pick a specific mode to force every target to be read the same way, for example to treat a list of ambiguous words as search terms rather than handles.

## `posts_per_target` (type: `integer`):

Upper bound on rows returned for each target. Threads serves roughly 10 posts on a profile, 20 on a search and 30 or so on a post with its replies.

## `max_targets` (type: `integer`):

How many targets to process in one run. Each costs one page fetch.

## `concurrency` (type: `integer`):

How many targets to fetch at the same time.

## `retries` (type: `integer`):

Retries with backoff before a target is reported as an error.

## `country` (type: `string`):

Optional two-letter country code to request from, for example us or gb. Leave empty to request from wherever the run happens to sit.

## Actor input object example

```json
{
  "targets": [
    "zuck",
    "#ai"
  ],
  "mode": "auto",
  "posts_per_target": 100,
  "max_targets": 50,
  "concurrency": 5,
  "retries": 3,
  "country": "us"
}
```

# Actor output Schema

## `posts` (type: `string`):

One row per post.

## `summary` (type: `string`):

Counts, engagement totals and the profiles found.

## `errors` (type: `string`):

Targets that could not be read, with a code and a redacted message.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "targets": [
        "zuck",
        "#ai"
    ],
    "posts_per_target": 100,
    "max_targets": 50,
    "concurrency": 5,
    "retries": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/threads-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "targets": [
        "zuck",
        "#ai",
    ],
    "posts_per_target": 100,
    "max_targets": 50,
    "concurrency": 5,
    "retries": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("s-r/threads-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "targets": [
    "zuck",
    "#ai"
  ],
  "posts_per_target": 100,
  "max_targets": 50,
  "concurrency": 5,
  "retries": 3
}' |
apify call s-r/threads-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,s-r/threads-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/0obg7sOtCHFm2orgW/builds/2Loa6Rnfn63BjFwFM/openapi.json
