# Threads Scraper: Posts, Replies, Search & Profiles (`sz022/threads-posts-replies-scraper`) Actor

Scrape Threads without login: posts from profiles, keyword and hashtag search, replies to any post, and profile info. Exact counts (hidden likes stay null, not 0), full media URLs, links, mentions, topic tags. No duplicates, limits respected. Export CSV, Excel, JSON.

- **URL**: https://apify.com/sz022/threads-posts-replies-scraper.md
- **Developed by:** [CHEN-SHU-CHEN](https://apify.com/sz022) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 post or reply scrapeds

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

### What is Threads Scraper?

Threads Scraper reads public Threads posts without a login and gives you one row per post. Give it **profiles** to get their recent posts, **keywords or hashtags** to get search results, or **post URLs** to get the replies under them. Turn on **Profile info** for a row per account with its bio, bio links and exact follower count.

Every row carries what Threads shows a logged-out visitor: the text with its links, mentions and hashtags, the topic tag, every image size and video file (including the best-quality DASH stream), carousel cards with their own size and alt text, likes, replies, reposts, quotes and shares, the author, and for replies the post they answer.

It is built to get the details right that Threads scrapers are usually criticised for:

- **No duplicates.** A post found twice, by two searches or two inputs, is delivered and charged once.
- **Limits are exact.** Ask for 20 posts and you get 20, not 25 and a bill for 25.
- **Hidden is not zero.** When an author hides their like count, `likeCount` is `null` and `likeAndViewCountsDisabled` is `true`. Threads itself sends `0` there; this Actor does not pass that on.
- **The right post.** A post URL is only answered with the post whose code matches the URL, and a profile only with that account's own data.

Because it runs on Apify, you can **schedule** runs, call it from the **API**, connect it to Make, Zapier or Google Sheets, and export as **CSV, Excel or JSON**.

### How to scrape Threads

1. Fill in any mix of **Profiles** (`https://www.threads.com/@nasa`, `@nasa` or `nasa`), **Search keywords or hashtags** (`coffee`, `#coffee`) and **Post URLs** (`https://www.threads.com/@zuck/post/Ddt7cL5EfUG`).
2. Set how many posts, results or replies you want for each.
3. Optionally narrow it down: a date range, a minimum number of likes, or languages.
4. Click **Start**. A profile with 20 posts takes a few seconds.
5. Open the **Output** tab or export the dataset. Each input line that returned nothing has a row of its own saying why.

### What data does it return?

Every post and reply row has `type` (`post` or `reply`), `status`, and the input it came from (`inputType`, `input`, `inputIndex`).

| Fields | Notes |
|---|---|
| `id`, `code`, `url` | `code` is the short id in the post's URL |
| `text`, `textFragments` | The text, and the same text split into plain text, links, mentions and stickers as Threads stores it |
| `links`, `mentions`, `hashtags` | Links are the real destinations, not Threads' click-tracking redirects |
| `topicTag` | The one topic a Threads post can carry: `id`, `name`, community flag |
| `createdAt`, `timestamp`, `isEdited` | ISO-8601 in UTC, and epoch seconds |
| `likeCount`, `replyCount`, `repostCount`, `quoteCount`, `shareCount` | Exact integers. `null` when Threads did not send the count or the author hid it, never a made-up `0` |
| `viewCount` | Threads shows views only on a post's own page: filled in for the posts you give under Post URLs, and for every post with **View counts** on |
| `likeAndViewCountsDisabled` | `true` when the author hid likes and views. Then `likeCount` and `viewCount` are `null` |
| `mediaType`, `mediaCount`, `media` | `text`, `image`, `video` or `carousel`. Each `media` item has `type`, `width`, `height`, `altText`, `imageUrl` (largest), `imageCandidates` (every size), `videoUrl`, `videoVersions`, `dash` (best video and audio stream, bandwidth, resolution, duration), `hasAudio`, `usertags` |
| `thumbnailUrl`, `videoUrl`, `bestVideoUrl` | Shortcuts to the first image and video. `bestVideoUrl` is the highest-bandwidth DASH video track (video only; audio is `dash.audioUrl`) |
| `isPinned`, `replyControl`, `isSpoiler`, `isPaidPartnership`, `isGhostPost` | |
| `location`, `music`, `gif`, `stickers`, `linkPreview`, `linkedInlineMedia` | Place tag, song (title, artist, ids, cover), GIF reply, inline stickers, link card (destination, title, image), an embedded Instagram post |
| `aiDetectionMethod`, `isAiLabeled` | Meta's AI-content label |
| `sensitiveContentOverlay` | The warning screen over sensitive media, when there is one |
| `quotedPost`, `quotedPostCode`, `repostedPost` | The quoted or reposted post with its own text, counts, media and author |
| `isReply`, `replyToAuthor`, `rootPostAuthor`, `isLikedByRootAuthor` | `isLikedByRootAuthor` says whether the original poster liked this reply |
| `rootPostCode`, `parentPostCode`, `replyDepth` | On replies read from a post URL: the post at the top, the post this one answers, and how deep it sits (1 = direct reply) |
| `selfThreadCount`, `selfThread` | On profile posts: the author's own follow-up posts in the same thread, each with its text, counts and media |
| `isRepost`, `repostedBy` | On profile posts: a post the account reposted from someone else |
| `searchQuery`, `searchVariant` | On search results: which tab it came from (`top`, `recent` or `tag`) |
| `authorId`, `authorUsername`, `authorFullName`, `authorIsVerified`, `authorProfilePicUrl`, `authorIsMetaAiBot`, `authorIsPrivate`, `authorTransparencyLabel`, `authorFollowerCount` | `authorFollowerCount` is filled in on a profile's own posts; Threads does not send it for other authors |
| `language`, `languageConfidence`, `languageSource` | See below. `threadsDetectedLanguage` is Threads' own, rarely set |
| `isOriginalPost` | `true` on the row for a post you gave under Post URLs |
| `notes`, `scrapedAt` | |

**Profile rows** (`type: "profile"`, with **Profile info** on): `id`, `username`, `url`, `fullName`, `biography`, `biographyFragments`, `biographyLinks`, `biographyMentions`, `biographyHashtags`, `bioLinks` (the links under the bio), `followerCount` (exact), `isVerified`, `isPrivate`, `profilePicUrl`, `hdProfilePicUrl`, `hdProfilePicVersions`, `profileTags`, `transparencyLabel`, `transparencyProduct`, `showThreadsBadge`, `hasOnboardedToThreads`.

**Input rows** (`type: "input"`) appear only for an input that returned nothing or stopped early: `status`, `error`, `rowsDelivered`, `postsSeen`, `duplicatesSkipped`, `filteredOut` (how many posts each filter removed) and `notes`. The same, for every input, is in `RUN_SUMMARY` in the key-value store.

| `status` | Meaning |
|---|---|
| `ok` | Read |
| `no_results` | Read, but nothing matched, or every match was already delivered by an earlier input |
| `partial` | Some rows were delivered, then Threads stopped answering, or the run hit its budget or timeout |
| `not_found` | The post was deleted or never public, or Threads answered 404 |
| `private` | A private profile |
| `login_required` | Threads sent the request to its login page (see the FAQ) |
| `blocked` | Threads kept refusing, on several IPs |
| `parse_error` | The page came back but not in the shape this Actor knows; reported rather than guessed |
| `invalid_input` | Not a profile, post URL or search this Actor can read |
| `not_processed` | Never reached, because the run stopped early. Not charged |

#### How is the language detected?

Threads' own `detected_language` is set on very few posts, so this Actor detects the language from the text itself. The writing system decides where it can: Hangul is Korean, kana is Japanese, Chinese characters without kana are Chinese, Thai script is Thai, and so on. Text in Latin, Cyrillic or Arabic script goes to [langdetect](https://pypi.org/project/langdetect/), seeded so the same text always gets the same answer. Links, mentions and hashtags are left out first. Posts with no words to go on (an emoji, a photo, only hashtags) get `null`, and are skipped when you filter by language. Short posts of a few words can be misread; `languageConfidence` tells you how sure langdetect was.

### How much does it cost?

You pay per row delivered, with no charge for compute or proxies. Rows that failed, were filtered out, or were duplicates are free.

| Event | What it is | Price | Per 1,000 |
|---|---|---|---|
| `post-scraped` | One post or reply row | $0.004 | $4 |
| `profile-scraped` | One profile row (only with Profile info on) | $0.004 | $4 |
| `view-count-fetched` | A view count read from a post's own page (only with View counts on, and only when a count was found) | $0.001 | $1 |

The view count costs extra because Threads only shows it on each post's own page, a separate request of about 1 MB per post. Posts given under Post URLs get their view count for free, since that page is read anyway.

#### Can I try it for free?

Apify's free plan includes $5 of usage a month, which covers about 1,250 posts.

#### How do I keep a run from costing more than I expect?

Set a maximum charge for the run when you start it. Each row is written and charged in one step by Apify's SDK, which writes only rows the remaining budget covers, so you are never charged for a row that did not reach your dataset, and no row is written past your limit. When the budget is used up the run stops, and the inputs it did not reach come back `not_processed`. A view count is only read when the budget covers both the post and its count, and only charged once its post is in the dataset. If writing or charging a row fails, the run stops rather than retry, because a retry could charge twice.

The run also stops in time for its timeout: requests are cut short before the run's time runs out, and whatever was not read comes back `not_processed`.

#### What happens if Apify restarts a run?

Apify sometimes moves a running Actor to another server, which restarts it. Progress is saved every 25 rows, after each input, and when Apify announces the move. A restart skips the inputs already finished, reads the dataset for the rows written since the last save, and does not write or charge those again; the input that was in progress carries on, counting the rows it already delivered toward its limit. If the run had already decided to stop (budget, timeout), the restart keeps to that decision. One case favours you: if the restart lands between a post and its view-count charge, that view count is never charged.

### Input example

```json
{
  "profiles": ["https://www.threads.com/@mosseri"],
  "maxPostsPerProfile": 50,
  "searchQueries": ["#coffee", "espresso machine"],
  "searchMode": "recent",
  "maxResultsPerSearch": 50,
  "postUrls": ["https://www.threads.com/@zuck/post/Ddt7cL5EfUG"],
  "maxRepliesPerPost": 200,
  "since": "30 days",
  "minLikes": 10,
  "languages": ["en"],
  "includeProfileInfo": true
}
```

### Output example

A post from a real run on 2026-10-18, trimmed to the main fields:

```json
{
  "type": "post",
  "status": "ok",
  "inputType": "profile",
  "input": "https://www.threads.com/@mosseri",
  "code": "Dd_mV-4iVTT",
  "url": "https://www.threads.com/@mosseri/post/Dd_mV-4iVTT",
  "text": "Three DM settings worth knowing about. Let me know if there are other ...",
  "createdAt": "2026-10-02T13:32:20Z",
  "likeCount": 344,
  "replyCount": 79,
  "repostCount": 64,
  "quoteCount": 3,
  "shareCount": 12,
  "viewCount": null,
  "likeAndViewCountsDisabled": false,
  "mediaType": "carousel",
  "mediaCount": 4,
  "media": [
    {
      "type": "image",
      "width": 1080,
      "height": 1350,
      "altText": null,
      "imageUrl": "https://instagram.frmq5-1.fna.fbcdn.net/v/t51.82787-15/834527974_...",
      "imageCandidates": [{"url": "https://...", "width": 1080, "height": 1350}, {"url": "https://...", "width": 720, "height": 900}]
    }
  ],
  "replyControl": "everyone",
  "isPinned": false,
  "authorUsername": "mosseri",
  "authorIsVerified": true,
  "authorFollowerCount": 1050331,
  "language": "en",
  "languageSource": "detected"
}
```

### Limits worth knowing

- **Search returns about 50 posts per term.** Logged out, Threads answers a search with about 20 posts and no next page. With **Combine search tabs** on (the default), the Actor also asks the other tab (Top or Recent) and the topic tag, and merges them without duplicates: about 50 in tests. To get more, search several related terms; duplicates between them are removed and not charged.
- **Replies come in Threads' Top order**, as on the post's page. Under each reply Threads shows a short chain of replies to it; those are included (with **Include replies to replies**), but a long discussion further down a chain is not followed.
- **Date range.** A profile is read newest first and reading stops once posts are older than **Posted on or after**, so a narrow recent window is quick. Pinned posts and reposts sit out of date order and do not end the scan. Search and replies are not in date order, so the range is a filter there.
- **Follower counts** of authors other than the profile you asked for are not in the data Threads sends with posts.
- **No logged-in data.** Private accounts, and anything Threads only shows to logged-in users, cannot be read.

### FAQ

#### Is it legal to scrape Threads?

This Actor reads only what any logged-out visitor can see, and uses no account. Personal data is still personal data: if you collect it, the rules where you work (for example the GDPR) apply to what you do with it. This is not legal advice.

#### Why did a profile come back login_required?

Threads sends a handle that does not exist, an account that only shows itself to logged-in users, and a visitor it is rate-limiting, to the same login page. So the row says `login_required` rather than guessing `not_found`. Check the spelling first. The Actor already retries once from another IP; if many correctly spelled accounts come back this way, Threads is limiting logged-out access for now: run again later.

#### Why does a run report fewer posts than I asked for?

The account has fewer posts in the range, the filters removed some (see `filteredOut` on the input row and in `RUN_SUMMARY`), or a search ran out of results. Duplicates are removed before counting, so 20 means 20 different posts.

#### What happens when Threads changes its site?

Threads loads more posts through internal query IDs that change every few weeks. The Actor carries the current ones, falls back to the query each page itself names, and if both fail, looks the current IDs up in Threads' own scripts, once per run. If a page still cannot be read, that input is reported as `parse_error` or `partial` with the rows already found, rather than failing the run.

#### Why is memory limited to 1 GB?

The Actor reads pages over plain HTTP, with no browser, so 512 MB is plenty. More memory would not make it faster.

#### Something broke or a number looks wrong

Open an issue on the **Issues** tab with the input and the run link.

### Other Actors for social media reporting

| Actor | Use it when |
|---|---|
| [Instagram, Facebook, Threads & YouTube Post Metrics](https://apify.com/sz022/social-post-metrics) | You have post URLs from a campaign and need likes, comments, shares and views for each, across platforms |
| [Instagram, Facebook & Threads Post Metrics Tracker](https://apify.com/sz022/social-post-metrics-tracker) | You want the same posts re-checked on a schedule, with the change since the last run |
| [Instagram & Threads Influencer Audit](https://apify.com/sz022/instagram-threads-influencer-audit) | You want engagement rate, median likes and posting frequency per account rather than every post |

# Actor input Schema

## `profiles` (type: `array`):

Threads profile URLs or handles, one per line: https://www.threads.com/@nasa, threads.net/@nasa, @nasa or nasa. Returns each profile's recent posts, newest first.

## `maxPostsPerProfile` (type: `integer`):

Posts (threads) to return per profile, after the date, likes and language filters. Set 0 to return only the profile row (turn on Profile info below).

## `searchQueries` (type: `array`):

Words to search Threads for, one per line. Start with # to search a topic tag (#coffee). Logged out, Threads shows about 20 results per search; see 'Combine search tabs' below to get about 50.

## `searchMode` (type: `string`):

Top is what Threads ranks most relevant; Recent is newest first.

## `maxResultsPerSearch` (type: `integer`):

Posts to return per search, after the filters and after removing duplicates. Logged out, about 50 is the most Threads gives for one search term.

## `mergeSearchVariants` (type: `boolean`):

Threads gives a logged-out search about 20 results and no next page. With this on, the other tab (Top or Recent) and the topic tag are searched too, and the results are combined without duplicates, which gives about 50. The tab each post came from is in searchVariant. Turn off to keep strictly to the tab chosen above.

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

Threads post URLs, one per line: https://www.threads.com/@zuck/post/Ddt7cL5EfUG. Returns the replies to each post, in Threads' Top order.

## `maxRepliesPerPost` (type: `integer`):

Replies to return per post URL, after the filters.

## `includeNestedReplies` (type: `boolean`):

Threads shows a short chain of replies under each reply. On: they are returned too, with parentPostCode and replyDepth. Off: direct replies only.

## `includeOriginalPost` (type: `boolean`):

Also return the post each URL points to, as its own row, with its view count. Charged as one post.

## `since` (type: `string`):

Only posts from this date on. A date (2026-10-01), a date and time in UTC (2026-10-01T12:00:00Z), or a relative time such as '7 days' or '3 months'. Profiles stop paging once posts get older than this.

## `until` (type: `string`):

Only posts up to this date. A bare date includes that whole day (UTC).

## `minLikes` (type: `integer`):

Only posts with at least this many likes. Posts whose author hid the like count are skipped when this is set.

## `languages` (type: `array`):

Only posts in these languages, as two-letter codes (en, es, ja, zh, ko, pt...). The language is detected from the post text by this Actor; posts without readable text are skipped when this is set.

## `includeReposts` (type: `boolean`):

A profile's tab also shows posts it reposted from other accounts. They are marked isRepost. Turn off to keep only the account's own posts.

## `includeProfileInfo` (type: `boolean`):

Add one row per input profile: bio, bio links, exact follower count, verified badge, HD picture, profile tags. Read from the same page as the posts, so no extra requests. Charged as profile-scraped.

## `fetchViewCounts` (type: `boolean`):

Threads shows view counts only on each post's own page. On: one extra request per post to read it, charged as view-count-fetched only when a count was found. Off: viewCount is null except on the posts given under Post URLs.

## `abortAfterConsecutiveFailures` (type: `integer`):

Ends the run early when Threads stops answering (blocked, login wall, unreadable pages). Inputs that do not exist do not count. 0 never stops early.

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

Apify's datacenter proxy, which reads Threads reliably. Residential and Google SERP groups are not used: the price per post is set for datacenter traffic, so a run that asks for them falls back to datacenter and notes it on each row. Your own proxy URLs are used as given.

## Actor input object example

```json
{
  "profiles": [
    "https://www.threads.com/@mosseri"
  ],
  "maxPostsPerProfile": 10,
  "searchQueries": [
    "coffee"
  ],
  "searchMode": "top",
  "maxResultsPerSearch": 10,
  "mergeSearchVariants": true,
  "maxRepliesPerPost": 50,
  "includeNestedReplies": true,
  "includeOriginalPost": true,
  "includeReposts": true,
  "includeProfileInfo": true,
  "fetchViewCounts": false,
  "abortAfterConsecutiveFailures": 5,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `results` (type: `string`):

Every field Threads shows logged out: text and its links, mentions and hashtags, media URLs, exact counts (null when hidden or unknown, never a made-up 0), author, reply chain, dates in ISO-8601.

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

What each input returned: status, rows delivered, duplicates skipped, posts filtered out and why.

# 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 = {
    "profiles": [
        "https://www.threads.com/@mosseri"
    ],
    "maxPostsPerProfile": 10,
    "searchQueries": [
        "coffee"
    ],
    "maxResultsPerSearch": 10,
    "includeProfileInfo": true,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("sz022/threads-posts-replies-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 = {
    "profiles": ["https://www.threads.com/@mosseri"],
    "maxPostsPerProfile": 10,
    "searchQueries": ["coffee"],
    "maxResultsPerSearch": 10,
    "includeProfileInfo": True,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("sz022/threads-posts-replies-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 '{
  "profiles": [
    "https://www.threads.com/@mosseri"
  ],
  "maxPostsPerProfile": 10,
  "searchQueries": [
    "coffee"
  ],
  "maxResultsPerSearch": 10,
  "includeProfileInfo": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call sz022/threads-posts-replies-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,sz022/threads-posts-replies-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/5hwRyvV3jOadQqh8T/builds/6Po1hdatkXXHFJf0r/openapi.json
