# Threads Scraper — Posts, Profiles, Search & Hashtags (`dressy_europium/threads-toolkit`) Actor

Scrape Threads posts, profiles, hashtags and search results without login. Exact like/reply/repost counts, images, videos, batch mode, pay per result.

- **URL**: https://apify.com/dressy\_europium/threads-toolkit.md
- **Developed by:** [Parker](https://apify.com/dressy_europium) (community)
- **Categories:** Social media, Developer tools, Automation
- **Stats:** 3 total users, 2 monthly users, 87.5% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## 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 — Posts, Profiles, Search & Hashtags

Languages: [English](README.md) | [中文](README.zh-TW.md)

Scrape [Threads](https://www.threads.com) without logging in: posts, profiles, hashtag
feeds, search results and replies — with **exact** engagement counts, full-resolution
media, and batch processing.

### What you get

Every post is one dataset record:

```json
{
    "id": "DZaExc0ESvs",
    "url": "https://www.threads.com/@zuck/post/DZaExc0ESvs",
    "author": {
        "username": "zuck",
        "displayName": "Mark Zuckerberg",
        "profileUrl": "https://www.threads.com/@zuck",
        "avatarUrl": "https://...",
        "isVerified": true
    },
    "content": "Interesting Biohub conversation with No Priors...",
    "timestamp": "2026-06-06T21:41:30.000Z",
    "stats": { "likes": 832, "replies": 302, "reposts": 57, "shares": 0, "quotes": 23 },
    "images": ["https://..."],
    "videos": [],
    "links": ["https://open.spotify.com/..."],
    "source": "profile_posts",
    "profile": "zuck",
    "scrapeMethod": "graphql"
}
```

**Counts are exact.** Most Threads scrapers read the numbers off the rendered page,
where Threads writes `1.5K` and `5.6M` — so `1,543` arrives as `1500`, and a profile
with `5,687,751` followers is recorded as `5,600,000`. This Actor reads Threads' own
JSON payloads, so you get the real integers. It also means extraction does not break
when Threads ships a UI change or serves a different interface language.

|                           |                                                                     |
| ------------------------- | ------------------------------------------------------------------- |
| **No login required**     | All four actions work anonymously. Cookies are optional.            |
| **Exact stats**           | `likes`, `replies`, `reposts`, `quotes` as integers, never rounded. |
| **Full-resolution media** | Largest image variant, carousel media, videos, GIFs, link previews. |
| **Batch mode**            | Many keywords / usernames / hashtags / post URLs in one run.        |
| **Transparent**           | Every record says which engine produced it (`scrapeMethod`).        |

### Quick start

1. Set **Action** to `Search Posts`.
2. Type a keyword, e.g. `openai`.
3. Set **Max Items** to `20`.
4. Click **Start**.

That's it — results land in the **Output** tab. To scrape a profile instead, switch
Action to `User Profile` and enter a username without the `@`.

> **Proxy.** Meta blocks datacenter IPs aggressively. Residential proxies succeed far
> more often, at a higher cost per GB. Datacenter proxies are usually fine for
> low-volume profile and post runs.

### Pricing

This Actor uses **pay per event** — you pay for results, not for compute time.

**$5.00 per 1,000 results** — every stored record costs the same, whether it is a post,
a reply, a search hit or a profile.

| Event            | When it fires                                   | Price  |
| ---------------- | ----------------------------------------------- | ------ |
| `actor-start`    | Once per run                                    | $0.005 |
| `post-output`    | Per post, reply or search/hashtag result stored | $0.005 |
| `profile-output` | Per profile record stored                       | $0.005 |

A profile run with `includePosts` charges one `profile-output` plus one `post-output`
per post returned. A 500-post run costs $2.51: 500 × $0.005, plus half a cent to start.

Two guarantees worth knowing:

- **You are only charged for stored results.** Charging happens after the record is
  written, never before.
- **`maxItems` is capped to your budget before scraping starts.** If your run's charge
  limit only covers 30 posts, the Actor scrapes 30 and finishes as a success with a
  status message saying so — it never overruns the limit.

> The Pricing tab on this Actor's page is the source of truth.

### Input

Set `action` to one of `search`, `hashtag`, `profile`, `post`. Only the fields for the
chosen action are used.

#### Common

| Field                | Type    | Description                              | Default     |
| -------------------- | ------- | ---------------------------------------- | ----------- |
| `action`             | string  | `search`, `hashtag`, `profile` or `post` | `profile`   |
| `maxItems`           | integer | Max posts to store (1–1000)              | `50`        |
| `proxyConfiguration` | object  | Proxy settings                           | Apify Proxy |

#### Per action

| Action    | Required                    | Optional                                |
| --------- | --------------------------- | --------------------------------------- |
| `search`  | `keyword`                   | `filter` (`recent` | `top`)            |
| `hashtag` | `tag` (with or without `#`) | `filter`                                |
| `profile` | `username` (without `@`)    | `includePosts` (default `true`)         |
| `post`    | `postUrl`                   | `maxReplies` (falls back to `maxItems`) |

```json
{ "action": "search", "keyword": "artificial intelligence", "filter": "recent", "maxItems": 50 }
```

```json
{ "action": "profile", "username": "zuck", "includePosts": true, "maxItems": 20 }
```

```json
{ "action": "post", "postUrl": "https://www.threads.com/@zuck/post/ABC123xyz", "maxReplies": 50 }
```

#### Rate limiting

| Field            | Type    | Description                                  | Default |
| ---------------- | ------- | -------------------------------------------- | ------- |
| `requestDelayMs` | integer | Delay before each page load                  | `1000`  |
| `maxRetries`     | integer | Retries when a page fails or is rate limited | `3`     |

Retries use exponential backoff, and a login wall or rate limit rotates to a fresh
session and proxy IP before retrying.

<details>
<summary>Advanced object form (still supported)</summary>

`rateLimitConfig` accepts `requestDelay`, `maxRetries`, `backoffDelay` and
`backoffMultiplier`. The two flat fields above override the matching keys when set,
leaving the others untouched — existing API callers and saved tasks are unaffected.

```json
{
    "action": "search",
    "keyword": "AI",
    "rateLimitConfig": { "requestDelay": 2000, "maxRetries": 5, "backoffDelay": 10000 }
}
```

</details>

#### Authentication (optional)

Cookies unlock the `location` and `joinedDate` profile fields and help when Threads
shows a login wall. **Scraping while logged in carries a risk to that account** — use
a throwaway one.

| Field          | Type    | Description                            | Default |
| -------------- | ------- | -------------------------------------- | ------- |
| `useCookies`   | boolean | Enable cookie/storage injection        | `false` |
| `storageState` | object  | Playwright `storageState.json` content | –       |

To export: log into Threads with Playwright, then
`await context.storageState({ path: 'storageState.json' })`, and paste the JSON.

If `useCookies` is on but `storageState` is empty, the run falls back to no-auth mode
with a warning. Only cookies and localStorage for Threads domains are injected;
sessionStorage is not supported.

### Output

#### Post record

| Field                | Type    | Description                                                      |
| -------------------- | ------- | ---------------------------------------------------------------- |
| `id`                 | string  | Threads short code, unique per post                              |
| `url`                | string  | Direct link to the post                                          |
| `author.username`    | string  | Author's handle                                                  |
| `author.displayName` | string  | Author's display name                                            |
| `author.profileUrl`  | string  | Link to the author's profile                                     |
| `author.avatarUrl`   | string  | Avatar image URL                                                 |
| `author.isVerified`  | boolean | Whether the author is verified                                   |
| `content`            | string  | Post text                                                        |
| `timestamp`          | string  | ISO 8601 publish time                                            |
| `stats.likes`        | integer | Likes                                                            |
| `stats.replies`      | integer | Replies                                                          |
| `stats.reposts`      | integer | Reposts                                                          |
| `stats.shares`       | integer | Reshares — Threads does not report this for every post           |
| `stats.quotes`       | integer | Quote-posts (JSON tiers only)                                    |
| `images`             | array   | Full-resolution image URLs, incl. carousels and link previews    |
| `videos`             | array   | Video URLs                                                       |
| `links`              | array   | External links                                                   |
| `quotedPost`         | object  | Minimal quoted-post info, if present                             |
| `source`             | string  | `search`, `hashtag`, `post`, `reply`, `profile`, `profile_posts` |
| `parentId`           | string  | For replies: the post being replied to                           |
| `profile`            | string  | For `profile_posts`: the profile that was scraped                |
| `scrapeMethod`       | string  | `embedded`, `graphql` or `dom` — see below                       |

#### Profile record

Marked with `type: "profile"`.

| Field            | Type           | Description                                      |
| ---------------- | -------------- | ------------------------------------------------ |
| `username`       | string         | Handle, without `@`                              |
| `displayName`    | string         | Display name                                     |
| `profileUrl`     | string         | Profile URL                                      |
| `avatarUrl`      | string         | Avatar image URL                                 |
| `bio`            | string         | Bio text                                         |
| `isVerified`     | boolean        | Verified badge                                   |
| `followersCount` | integer        | Followers — exact unless `scrapeMethod` is `dom` |
| `followingCount` | integer        | **Usually absent**, see FAQ                      |
| `postsCount`     | integer        | **Usually absent**, see FAQ                      |
| `location`       | string | null | Requires login cookies                           |
| `joinedDate`     | string | null | Requires login cookies                           |
| `partial`        | boolean        | True when optional fields are missing            |
| `missingFields`  | array          | Which fields were unavailable                    |

Posts from the profile page are stored as separate records with
`source: "profile_posts"`.

#### `scrapeMethod` — knowing when data degrades

| Value      | Meaning                                                                             |
| ---------- | ----------------------------------------------------------------------------------- |
| `embedded` | Read from the JSON shipped with the page. Exact counts.                             |
| `graphql`  | Read from an intercepted GraphQL response. Exact counts.                            |
| `dom`      | Fallback: parsed from rendered HTML. **Counts may be rounded**, some fields absent. |

A healthy run is almost entirely `embedded`/`graphql`. If you see mostly `dom`, Threads
has changed its payload and the fallback is carrying the run — please open an issue.

#### Run summary

Each run writes a `RUN_SUMMARY` record to the key-value store, linked from the Output
tab: items per source, the engine mix, charged events and the stop reason.

```json
{
    "totalItems": 13,
    "posts": 12,
    "profiles": 1,
    "bySource": { "search": 3, "hashtag": 3, "profile": 1, "profile_posts": 3, "post": 1, "reply": 2 },
    "engineMix": { "embedded": 13 },
    "engineMixPercent": { "embedded": 100 },
    "stopReason": null
}
```

#### Data quality

Posts missing content, author or a valid timestamp are dropped rather than stored.
Profiles are kept even when incomplete, but flagged with `partial: true` and
`missingFields`.

### Batch mode

Process many targets in one run. Batch mode activates when at least one list is
non-empty; the single-value fields are then ignored.

```json
{
    "action": "search",
    "keywords": ["vibe coding", "machine learning"],
    "usernames": ["zuck", "openai"],
    "tags": ["AI", "tech"],
    "postUrls": ["https://www.threads.com/@user/post/ABC123"],
    "maxItems": 20,
    "concurrency": 2
}
```

| Field         | Type    | Description                         | Default |
| ------------- | ------- | ----------------------------------- | ------- |
| `keywords`    | array   | Search keywords                     | unset   |
| `usernames`   | array   | Usernames, without `@`              | unset   |
| `tags`        | array   | Hashtags, without `#`               | unset   |
| `postUrls`    | array   | Post URLs                           | unset   |
| `concurrency` | integer | Targets processed in parallel (1–5) | `2`     |

`maxItems` applies **per target**. Higher `concurrency` is faster but much more likely
to trip rate limiting.

### FAQ

**Why do I get fewer results than `maxItems`?**
Threads caps what it serves anonymously — roughly 10 posts for a search and 20 for a
profile, varying by account and query. The Actor delivers everything it can reach;
supplying login cookies raises the ceiling.

**Do I need to log in?**
No. All four actions work anonymously. Cookies only add `location` / `joinedDate` and
help past login walls — and put the account you use at some risk.

**Why are `followingCount` and `postsCount` empty?**
Threads does not send them to anonymous requests at all — the fields are absent from
the payload, not merely hidden. Rather than invent numbers, the Actor lists them in
`missingFields`.

**What happens if Threads shows a login wall?**
The session and its proxy IP are retired and the request is retried on a fresh
identity. If every retry is blocked, the run fails with a status message saying so
rather than quietly returning an empty dataset.

**How do I avoid rate limiting?**
Use residential proxies, raise `requestDelayMs`, lower `concurrency`, and leave a few
minutes between runs.

**Can I scrape private accounts?**
No. Only public data. Private accounts, deleted posts and region-restricted content
are not accessible.

**Why is some post text truncated?**
Threads truncates long posts in feeds. Scrape the post URL directly with the `post`
action to get the full text.

**Are usernames with dots supported?**
Yes — `@user.name` works.

**Is this legal?**
The Actor collects only publicly visible data and does not bypass access controls. You
are responsible for how you use the data, including compliance with Threads' Terms of
Service, applicable copyright, and data-protection law such as GDPR when records
concern identifiable people. Cookie authentication is opt-in and may breach Threads'
terms for the account used. See the disclaimer below.

**Is there an official API?**
Yes — the [Threads API](https://developers.facebook.com/docs/threads) is the better
choice for accessing **your own** account data: higher limits, full history, no
blocking. This Actor covers the public data the API does not expose.

### Limitations

Honest expectations, so nothing surprises you mid-run:

- **Volume is capped by Threads**, not by this Actor. Infinite scroll stops serving
  after a while, so results may be fewer than requested.
- **No private, deleted or region-restricted content.**
- **Rate limiting** is real; frequent runs from the same IP will hit it.
- **Login walls** appear intermittently and are retried on a new session, but a
  sustained wall will end the run.
- **`followingCount` / `postsCount`** are unavailable anonymously.
- Threads can change its payload at any time. `scrapeMethod` and the weekly smoke test
  exist so that gets caught quickly.

### Development

```bash
npm install
npm run lint
npm run build
npm test              # offline: fixtures + local servers
npm run smoke:live    # live checks against threads.com
```

`npm run smoke:auth` runs the authenticated profile check; supply
`THREADS_STORAGE_STATE_PATH=/path/to/storageState.json` or
`THREADS_STORAGE_STATE_JSON='{"cookies":[...]}'`.

Fixtures under `test/fixtures/` are generated by `node scripts/capture-fixtures.mjs`
and must never be hand-edited — the mapper tests assert exact values from them.

### Support

Report problems via the **Issues** tab on the Actor page. Including the run ID and the
`RUN_SUMMARY` record makes diagnosis much faster.

### Changelog

See [CHANGELOG.md](CHANGELOG.md). Current version: **0.2**.

### License

Apache License 2.0 — see [LICENSE](LICENSE).

### Disclaimer

This tool collects publicly available data for research and analysis. You are
responsible for using it in compliance with Threads' Terms of Service and applicable
law. The authors accept no responsibility for misuse.

***

**Keywords**: Threads scraper, Threads API, Meta Threads, social media scraper,
Instagram Threads, Threads posts, Threads data extraction, social media monitoring,
Threads automation, influencer discovery, follower count scraper

# Actor input Schema

## `action` (type: `string`):

What to scrape. All four actions work without login. Cookies are an optional fallback for when Threads shows a login wall.

## `keyword` (type: `string`):

Required for the 'Search Posts' action. The keyword to search for.

## `username` (type: `string`):

Required for the 'User Profile' action. Username without @ (e.g. 'zuck').

## `includePosts` (type: `boolean`):

Also scrape recent posts from the profile page.

## `tag` (type: `string`):

Required for the 'Hashtag Search' action. Hashtag without # (e.g. 'AI').

## `postUrl` (type: `string`):

Required for the 'Single Post' action. Full URL, e.g. https://www.threads.com/@zuck/post/ABC123

## `maxReplies` (type: `integer`):

Maximum replies to scrape from the post page, in addition to the post itself. Falls back to Max Items when not set.

## `keywords` (type: `array`):

Run a search for each keyword in one run. Setting any batch list switches the run to batch mode and the single-value fields above are ignored.

## `usernames` (type: `array`):

Fetch each profile in one run. Without @.

## `tags` (type: `array`):

Search each hashtag in one run. Without #.

## `postUrls` (type: `array`):

Scrape each post URL in one run.

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

How many batch targets to process in parallel. Higher values are faster but far more likely to trip Threads' rate limiting.

## `filter` (type: `string`):

Sort order for the Search and Hashtag actions.

## `maxItems` (type: `integer`):

Maximum number of posts to scrape. Anonymous runs are capped by Threads itself (roughly 10 for search, 20 for a profile) unless you supply login cookies.

## `proxyConfiguration` (type: `object`):

Strongly recommended. Meta blocks datacenter IPs aggressively, so residential proxies succeed far more often — at a higher cost per GB. Datacenter proxies are usually fine for low-volume profile and post runs.

## `requestDelayMs` (type: `integer`):

Pause before each page load. Defaults to 1000 ms. Raise this if you hit rate limits. Overrides rateLimitConfig.requestDelay when set.

## `maxRetries` (type: `integer`):

How many times to retry a page that fails or gets rate limited, with exponential backoff. Defaults to 3. Overrides rateLimitConfig.maxRetries when set.

## `rateLimitConfig` (type: `object`):

Advanced object form: requestDelay, maxRetries, backoffDelay, backoffMultiplier. Kept for API back-compatibility; the two fields above override it when set.

## `useCookies` (type: `boolean`):

Use a saved login state for auth-only fields and as a fallback when Threads shows a login wall. Requires Storage State below. Scraping while logged in carries a risk to that account.

## `storageState` (type: `object`):

Playwright storageState.json content exported after logging into Threads. If Use Login Cookies is on but this is empty, the run falls back to no-cookie mode.

## Actor input object example

```json
{
  "action": "search",
  "keyword": "openai",
  "username": "zuck",
  "includePosts": true,
  "tag": "ai",
  "postUrl": "https://www.threads.com/@zuck/post/DW4Gb79kQc0",
  "concurrency": 2,
  "filter": "recent",
  "maxItems": 20,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "useCookies": false
}
```

# Actor output Schema

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

All scraped records. Post records carry author, content, stats and media; profile records carry the account header and are marked with type=profile.

## `runSummary` (type: `string`):

Per-run report: item counts per source, the extraction engine mix (a run that is mostly 'dom' means Threads changed its payload and the fallback is carrying the load), charged events, and why the run stopped.

# 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 = {
    "action": "search",
    "keyword": "openai",
    "username": "zuck",
    "tag": "ai",
    "postUrl": "https://www.threads.com/@zuck/post/DW4Gb79kQc0",
    "maxItems": 20,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("dressy_europium/threads-toolkit").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 = {
    "action": "search",
    "keyword": "openai",
    "username": "zuck",
    "tag": "ai",
    "postUrl": "https://www.threads.com/@zuck/post/DW4Gb79kQc0",
    "maxItems": 20,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("dressy_europium/threads-toolkit").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 '{
  "action": "search",
  "keyword": "openai",
  "username": "zuck",
  "tag": "ai",
  "postUrl": "https://www.threads.com/@zuck/post/DW4Gb79kQc0",
  "maxItems": 20,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call dressy_europium/threads-toolkit --silent --output-dataset

```

## MCP server setup

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

```

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/6spG756F3KIe4wGex/builds/h2P0tF30Qh8UJj7eb/openapi.json
