# Threads Scraper: Posts, Threads Replies, Profile Scraper, API (`hyperbach/threads-scraper`) Actor

Meta Threads scraper API, no login: threads posts by link, a profile's posts with exact counts and views, threads replies and comment trees, a threads search scraper for keywords and tags, threads profiles with the exact follower count. One row per post, reply or profile; free rows for misses.

- **URL**: https://apify.com/hyperbach/threads-scraper.md
- **Developed by:** [Hyperbach](https://apify.com/hyperbach) (community)
- **Categories:** Social media, Lead generation, Automation
- **Stats:** 6 total users, 5 monthly users, 99.1% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 posts

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Threads Scraper: Posts, Threads Replies, Profile Scraper, API

**Threads (Meta) data without a login: profiles with the exact follower count, a profile's posts to the end of its list, posts by link or code with their view count, replies to the end of the list with nested conversations, keyword and tag search — one row per post or profile, every count exact, and a free row that says why for an input that names nothing.** Run it on a schedule with `onlyNew` and each run delivers only the posts no earlier run delivered ([monitor accounts for new posts](https://apify.com/hyperbach/threads-scraper/examples/monitor-threads-accounts-for-new-posts)). No account, no cookie, no browser, proxies handled for you.

### Why this Threads scraper

- **Exact counts, every row, null where Threads withholds.** Likes, replies, reposts, quotes and shares are the exact integers Threads serves (1,841 / 318 / 153 / 32 / 89 on our reference post, equal to the lane leader's row); the view count comes free on every post you name and on request for list rows; a count Threads does not give is null, never 0. In our test of the lane, actors wrote zeros for missing counts, rounded followers, or delivered Instagram's profile as Threads'.
- **Profile history to the end of the list.** A profile's Threads tab is read with Threads' own paging to the list's end (168 of the 168 shown threads in our test; 1,400+ for a busy account), newest first, with `postedAfter` to stop at a date; the profile row carries the exact follower count, the shown threads count, bio, links, tags and the linked Instagram account.
- **Replies as a conversation.** Replies under a post you name are read to the end of what Threads serves a visitor (250 of a post's 318 in our test; the row says how many and why), in Top or Recent order, with `includeNestedReplies` for the conversations under each reply; every reply row carries its level, its parent and the root post. A walk Threads rate-limits says so on the post row and in the run summary.
- **One run, any mix, one row per entity.** Usernames, profile links, post links and codes on threads.com or threads.net, keywords and #tags in one run; the same post given three ways is one row; a post that is also found on a profile tab or a search page is one row; every input that names nothing comes back as a free row that says why (not found, invalid, empty, duplicate).
- **Search that says what it matched.** Threads shows a visitor one page per surface (about 20 posts on Top, 20 under the topic tag), no Recent sort and no date window: the rows say so, every search row is flagged `keyword_match`, and `keywordMatchOnly` leaves the off-keyword rows out before the charge. `searchPasses` loads a page again for the posts Threads rotates in (a second load added 43 posts to 58 for starbucks and #coffee in our test), and the `accounts` surface gives the accounts Threads' search box offers, as exact profiles.
- **Built for schedules.** `onlyNew` delivers only posts no earlier run delivered; with the memory on, a profile row says how its followers moved since the last run and a post read again carries its engagement change. Filters before the charge: `minLikes`, `verifiedOnly`.
- **Light and fast.** Plain HTTP, no browser: about 2 KB of traffic per profile post, 4.5 KB per reply; 131 rows in 14 seconds and 250 replies in 48 seconds on one exit in our test, from datacenter IPs.

### Who it's for

- **Marketers and brand teams** — what Threads says about a brand or a topic today: keyword and tag search with the match flag, the replies under a post with their authors, the exact engagement of a creator's recent posts.
- **Researchers and analysts** — a profile's full post history with exact counts and times, replies as threaded conversations with levels and parents, media and link URLs, in one typed row shape.
- **Monitoring teams** — a scheduled list of profiles or keywords that delivers only what is new, with the follower change and the engagement change since the last run.
- **Developers and agents** — typed fields, one row per input in input order, a free labelled row for every miss, and a run summary with a stop reason per input to check a pipeline against.

### Quick start

**A profile with its newest posts**

```json
{
  "profiles": [
    "zuck"
  ],
  "maxPostsPerProfile": 25
}
```

**Posts by link or code, with views**

```json
{
  "posts": [
    "https://www.threads.com/@zuck/post/DdZ7sQvkTFn",
    "Dd1MqfcG0aH"
  ]
}
```

**A post with its replies and the conversations under them**

```json
{
  "posts": [
    "https://www.threads.com/@zuck/post/DdZ7sQvkTFn"
  ],
  "maxRepliesPerPost": 300,
  "includeNestedReplies": true
}
```

**Keyword and tag search, matches only**

```json
{
  "keywords": [
    "superintelligence",
    "#ai"
  ],
  "searchSurfaces": [
    "top",
    "tags"
  ],
  "keywordMatchOnly": true
}
```

**A profile's whole history with views**

```json
{
  "profiles": [
    "threads"
  ],
  "maxPostsPerProfile": 1000,
  "includeViews": true
}
```

**A daily monitor: new posts only**

```json
{
  "profiles": [
    "zuck",
    "threads"
  ],
  "maxPostsPerProfile": 50,
  "onlyNew": true,
  "stateStoreName": "my-threads-watch"
}
```

### Output

Two row types share one dataset (`row_type` says which): `post` (a post, a reply, a search hit) and `profile`:

| field | meaning |
|---|---|
| `row_type` | `profile` or `post`: which columns of the row are filled. |
| `input` | The value you gave that this row answers (the first form, when the same post or profile was given several ways); on a search row the keyword; on a reply row the post you named. |
| `input_index` | The 1-based position of the input in its list (`profiles`, `posts` or `keywords`). |
| `inputs_merged` | Other forms of the same post or profile you gave (a code and a link of one post), merged into this row. |
| `source_kind` | Where the row came from: `post` (a post you named), `replies` / `nested_replies` (under a post you named), `profile_threads` / `profile_replies` / `profile_reposts` (a profile's tab), `search_top` / `search_tags` (a search surface), `search_accounts` (an account Threads' search box offered for a keyword), `profile` (a profile you named), `author` (an author's profile with includeAuthorProfiles). |
| `status` | `ok` = a delivered record; `not_found`, `invalid`, `empty`, `failed`, `duplicate` = a free row that says why there is no record for this input. |
| `status_detail` | Why a row is not `ok`, in words. |
| `scraped_at` | When this row was read (UTC). |
| `post_code` | The post's 11-character code, as in its link (`DdZ7sQvkTFn`). |
| `post_id` | Threads' numeric id of the post, as a string. |
| `post_url` | The post's link on threads.com. |
| `created_at` | When the post was published (UTC, to the second). |
| `created_at_timestamp` | The same moment as a Unix timestamp (seconds). |
| `text` | The post's full text, as written (empty on a media-only post). |
| `text_length` | Characters in `text`. |
| `media_type` | `text`, `image`, `video`, `carousel`, or `repost` (a reposting item of the reposts tab: the post itself is in `reposted_post`). |
| `media` | One object per picture or video of the post (every carousel slide): `type`, `url` (the largest rendition), `width`, `height`; a video adds `has_audio` and `cover_url` (its cover image). |
| `image_urls` | The post's pictures, largest rendition URLs. |
| `video_urls` | The post's videos (MP4 URLs; signed, they expire). |
| `media_count` | How many pictures and videos the post has. |
| `gif_url` | The GIF's own URL where Threads serves it; null when a visitor gets only Threads' placeholder image (every GIF of a logged-out read: `accessibility_caption` names the GIF). |
| `has_audio` | The post's video has an audio track. |
| `accessibility_caption` | The alt text Threads generated for the media. |
| `links` | The links in the text (the real targets, unwrapped from Threads' redirector). |
| `mentions` | The `@users` named in the text. |
| `stickers` | The inline stickers of the text, as image URLs. |
| `topic_tag` | The post's topic tag, when the author set one (Threads has one tag per post, not hashtags). |
| `link_preview` | The link card under the post: `title`, `url` (unwrapped), `display_url`, `description`, `image_url`, `favicon_url`. |
| `like_count` | Likes, exact. |
| `reply_count` | Replies, exact, as Threads counts them (more than a visitor is served; see `replies_note`). |
| `repost_count` | Reposts, exact. |
| `quote_count` | Quotes, exact. |
| `share_count` | Shares (reshares), exact; null where Threads sends none: on every reply, and on posts it shows no share count for (22 of 40 profile posts in our test; the post's own page sends null for them too). |
| `view_count` | Views, exact, when read: always on a post you named; on list rows with `includeViews`; else null (never 0). |
| `views_source` | `page` (the view count came with the post's own page, free) or `fetched` (read for this row with includeViews, charged as `post_views`); null when no count was read. |
| `engagement_rate` | (likes + replies + reposts + quotes) ÷ the author's followers, when the followers are known (a profile's own posts; includeAuthorProfiles). |
| `is_reply` | The post is a reply to another post. |
| `reply_to_username` | The author the post replies to. |
| `reply_to_user_id` | That author's Threads id. |
| `parent_post_code` | On a reply row: the code of the post it answers (the post you named for a direct reply, the parent reply for a nested one); on a post you named that is itself a reply: its parent. |
| `root_post_code` | On a reply row: the code of the post you named; on a reply you named: the root of its conversation. |
| `reply_level` | 0 = a top-level post; 1 = a direct reply to the root; 2 and deeper = a nested reply. |
| `is_quote` | The post quotes another post. |
| `quoted_post` | The quoted post, in short: code, url, author, text, created_at, the five counts. |
| `is_repost` | The row is a repost (the reposts tab). |
| `reposted_post` | The reposted post, in short: code, url, author, text, created_at, the five counts. |
| `thread_id` | The id of the thread the post belongs to on a tab or search page: the posts of one self-thread (a multi-post thread by one author) share it. |
| `thread_position` | The post's position in its thread (0 = the first post). |
| `self_thread_count` | How many posts the author's self-thread has on a tab or search page (the posts sharing `thread_id` by the thread's author); null on a post read alone. |
| `is_pinned` | The post is pinned to its author's profile. |
| `is_edited` | The post was edited. |
| `is_paid_partnership` | The post is labelled a paid partnership. |
| `is_spoiler` | The media is marked as a spoiler. |
| `reply_control` | Who may reply, as the author set it (`everyone`, …). |
| `ai_label` | Threads' AI-generated label on the post, where it shows one. |
| `author_username` | The author's username. |
| `author_id` | The author's Threads id (not the Instagram id). |
| `author_name` | The author's display name. |
| `author_verified` | The author is verified. |
| `author_profile_pic_url` | The author's profile picture. |
| `author_is_private` | The author's profile is private. |
| `author_followers` | The author's exact follower count, when read this run (a profile's own posts; includeAuthorProfiles). |
| `query` | On a search row (a post or an account the search found): the keyword or tag this row was found for. |
| `search_surface` | On a search row: `top`, `tags` or `accounts`. |
| `search_tag` | On a Tags-surface row: the topic tag Threads matched, when the page named it. |
| `keyword_match` | On a search row: the keyword appears in the text, the topic tag, the quoted post or the author's names (Threads' Top surface ranks related posts too). |
| `list_note` | What the list this row came from shows a visitor (one page per search surface; the first page of the replies and reposts tabs). |
| `replies_requested` | On a post you named: `maxRepliesPerPost`. |
| `replies_delivered` | On a post you named: how many reply rows were delivered under it. |
| `replies_note` | Why fewer replies than requested were delivered (the list's end, with the post's own count; a stop). |
| `related_posts` | On a post you named: the posts Threads shows under it as related (code, username, url). |
| `previous_read_at` | With the memory on: when this post or profile was last delivered by an earlier run. |
| `engagement_change` | With the memory on, on a post read again: the change of the five counts since the last delivered read (`like_count`, `reply_count`, `repost_count`, `quote_count`, `view_count`). |
| `raw` | Threads' own object behind the row, with `includeRaw`. |
| `run_tag` | Your `runTag`, copied onto every row. |
| `username` | The profile's username. |
| `user_id` | The profile's Threads id (not the Instagram id), as a string. |
| `full_name` | The display name. |
| `biography` | The bio, full. |
| `bio_links` | The links in the bio (unwrapped). |
| `is_verified` | The profile is verified. |
| `is_private` | The profile is private (its posts are not shown to a visitor). |
| `profile_pic_url` | The profile picture. |
| `profile_pic_hd_url` | The largest profile picture Threads serves. |
| `follower_count` | Followers, exact. |
| `threads_count` | The profile's threads count as the page shows it, when it is a plain number (`168`); null when Threads rounds it (`1.2K`, kept in `threads_count_shown`). |
| `threads_count_shown` | The threads count as shown (`168`, `1.2K`). |
| `profile_tags` | The profile's interest tags (`AI`, `MMA`, …). |
| `profile_url` | The profile's link on threads.com. |
| `instagram_url` | The linked Instagram account (a Threads account is its Instagram account of the same username). |
| `podcast_name` | The podcast the profile links, where it shows one. |
| `podcast_url` | The podcast's link. |
| `tab` | The tabs read for this profile's posts (`threads`, `replies`, `reposts`, comma-separated). |
| `posts_requested` | `maxPostsPerProfile` × the tabs read. |
| `posts_delivered` | How many post rows were delivered for this profile. |
| `posts_note` | Why fewer posts than requested were delivered (the list's end with the shown count, postedAfter, onlyNew, the filters, a tab's first page); on an account-search row, what the search box offers. |
| `last_post_at` | The newest post's time among the Threads-tab posts read (pinned posts aside). |
| `recent_posts_read` | How many Threads-tab posts the averages below are over. |
| `recent_avg_likes` | Average likes over those posts. |
| `recent_avg_replies` | Average replies over those posts. |
| `followers_previous` | With the memory on: the follower count as last delivered by an earlier run. |
| `followers_change` | With the memory on: `follower_count` minus `followers_previous`. |

Example record:

```json
{
  "row_type": "post",
  "input": "https://www.threads.com/@zuck/post/DdZ7sQvkTFn",
  "input_index": 1,
  "inputs_merged": [
    "https://www.threads.net/@zuck/post/DdZ7sQvkTFn",
    "DdZ7sQvkTFn"
  ],
  "source_kind": "post",
  "status": "ok",
  "status_detail": null,
  "scraped_at": "2026-10-10T00:02:13Z",
  "post_code": "DdZ7sQvkTFn",
  "post_id": "3988481471392330087",
  "post_url": "https://www.threads.com/@zuck/post/DdZ7sQvkTFn",
  "created_at": "2026-09-17T22:27:44Z",
  "created_at_timestamp": 1789684064,
  "text": "Muse for Mac is out today! It works across apps, files, calendar, notes, and messages on your computer. You control what it can access. The team is shipping fast. Download at ai.meta.com/muse/download",
  "text_length": 200,
  "media_type": "text",
  "media": null,
  "image_urls": null,
  "video_urls": null,
  "media_count": 0,
  "gif_url": null,
  "has_audio": null,
  "accessibility_caption": null,
  "links": [
    "http://ai.meta.com/muse/download"
  ],
  "mentions": null,
  "stickers": null,
  "topic_tag": null,
  "link_preview": null,
  "like_count": 1841,
  "reply_count": 318,
  "repost_count": 153,
  "quote_count": 32,
  "share_count": 89,
  "view_count": 157870,
  "views_source": "page",
  "engagement_rate": null,
  "is_reply": false,
  "reply_to_username": null,
  "reply_to_user_id": null,
  "parent_post_code": null,
  "root_post_code": null,
  "reply_level": 0,
  "is_quote": false,
  "quoted_post": null,
  "is_repost": false,
  "reposted_post": null,
  "thread_id": null,
  "thread_position": null,
  "self_thread_count": 0,
  "is_pinned": false,
  "is_edited": false,
  "is_paid_partnership": false,
  "is_spoiler": false,
  "reply_control": "everyone",
  "ai_label": "{\"detection_method\": \"NONE\"}",
  "author_username": "zuck",
  "author_id": "63055343223",
  "author_name": "Mark Zuckerberg",
  "author_verified": true,
  "author_profile_pic_url": "https://scontent-phl2-1.cdninstagram.com/v/t51.82787-19/825322135_17989325280103224_1252773933700107438_n.jpg?stp=dst-jpg_s206x206_tt6&_nc_cat=107&ccb=7-5&_nc_sid=30ff31&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLnd3dy4xMDgwLkMzIn0%3D&_nc_ohc=l7qeCXGmYHsQ7kNvwEFTVOn&_nc_oc=Adrf5-GaepFVtOPnPzNV5nLDWB-LX9x8r8A6YILbQNSGbvG-aisMvjRHoOvot8g78Ic&_nc_zt=24&_nc_ht=scontent-phl2-1.cdninstagram.com&_nc_gid=keE14ec5-x7nR_U-ifJgyQ&_nc_ss=7b289&oh=00_AQPz-ardxL9MjFDRJ_BfQjidx99SBVd3XeRT0kPhBq-J2w&oe=6ACF40EB",
  "author_is_private": false,
  "author_followers": null,
  "query": null,
  "search_surface": null,
  "search_tag": null,
  "keyword_match": null,
  "list_note": null,
  "replies_requested": 20,
  "replies_delivered": 20,
  "replies_note": null,
  "related_posts": [
    {
      "code": "DdaWHdskkU3",
      "username": "eastmel",
      "url": "https://www.threads.com/@eastmel/post/DdaWHdskkU3"
    },
    {
      "code": "DdaoIYUgrJS",
      "username": "elek7817",
      "url": "https://www.threads.com/@elek7817/post/DdaoIYUgrJS"
    },
    {
      "code": "DdapXchIy7g",
      "username": "thisiskhoagates",
      "url": "https://www.threads.com/@thisiskhoagates/post/DdapXchIy7g"
    },
    {
      "code": "DddicpMjx7A",
      "username": "luxury_maldives_voyage",
      "url": "https://www.threads.com/@luxury_maldives_voyage/post/DddicpMjx7A"
    },
    {
      "code": "Ddg8OiciPQ0",
      "username": "zurck53",
      "url": "https://www.threads.com/@zurck53/post/Ddg8OiciPQ0"
    }
  ],
  "previous_read_at": null,
  "engagement_change": null,
  "raw": null,
  "run_tag": null,
  "username": null,
  "user_id": null,
  "full_name": null,
  "biography": null,
  "bio_links": null,
  "is_verified": null,
  "is_private": null,
  "profile_pic_url": null,
  "profile_pic_hd_url": null,
  "follower_count": null,
  "threads_count": null,
  "threads_count_shown": null,
  "profile_tags": null,
  "profile_url": null,
  "instagram_url": null,
  "podcast_name": null,
  "podcast_url": null,
  "tab": null,
  "posts_requested": null,
  "posts_delivered": null,
  "posts_note": null,
  "last_post_at": null,
  "recent_posts_read": null,
  "recent_avg_likes": null,
  "recent_avg_replies": null,
  "followers_previous": null,
  "followers_change": null
}
```

### Pricing

**Pay only for the rows a run delivers**, with no start fee and no platform usage billed to you. Prices fall with your Apify plan.

| per 1,000 | Free | Starter | Scale | Business |
|---|---|---|---|---|
| Post (`post`) | $2.00 | $1.60 | $1.40 | $1.00 |
| Profile (`profile`) | $4.00 | $3.40 | $2.80 | $2.00 |
| Reply (`reply`) | $2.00 | $1.70 | $1.40 | $1.00 |
| View count (list rows) (`post_views`) | +$1.60 | +$1.36 | +$1.12 | +$0.80 |

A post is **$2.00 per 1,000** on the Free plan and $1.00 on Business; a post you name by link or code carries its view count at no extra charge. The `+` fee is charged only on a list row that carries a view count read with `includeViews` ($1.60 per 1,000 on Free). A profile row is charged per profile, a reply row per reply (nested replies included). An input that names nothing (not found, invalid, empty, a duplicate) is a free row that says why; so is a run that failed. `maxItems` counts every charged row; before the first request the log says the most the run can cost at your plan's prices. Enterprise plans have their own rates — the Actor's Pricing tab shows the price for your plan.

### Usage patterns

- **Profiles** — Paste usernames or profile links in `profiles`. Each gives one `profile` row (the exact follower count, the shown threads count, bio, links, tags, picture, the linked Instagram account) and up to `maxPostsPerProfile` `post` rows from the tabs in `profileTabs` (`threads` by default; `replies` and `reposts` give their first page, about 30 posts, which is all Threads shows a visitor). The Threads tab is read newest first to the list's end; `postedAfter` stops it at a date. The replies tab gives the account's own replies, each with the code of the post it answers (`parent_post_code`). The profile row says how many posts were delivered and why fewer, when fewer (`posts_note`), and carries the newest post's time and the recent averages. With `maxPostsPerProfile` 0 a run gives the profile rows only ([exact follower counts for a list of accounts](https://apify.com/hyperbach/threads-scraper/examples/threads-follower-counts-for-a-list-of-accounts)); `includeProfileRow` off gives the posts only ([image and video URLs from an account's posts](https://apify.com/hyperbach/threads-scraper/examples/threads-image-and-video-urls-from-account-posts)).
- **Posts and replies** — Paste post links or codes in `posts`. Each gives one `post` row with its five exact counts and its view count (free: the post's own page carries it), media, links, mentions, the quoted or reposted post, the related posts, and — for a post that is itself a reply — its parent and root. `maxRepliesPerPost` adds the replies under it as `post` rows (`source_kind` replies, `reply_level` 1), in Top or Recent order, to the end of what Threads serves a visitor; `includeNestedReplies` adds the conversations under each reply (`reply_level` 2 and deeper, `parent_post_code` the reply answered). The post row says how many replies were delivered and why fewer than Threads' count (`replies_note`), including when Threads rate-limited the walk (the post is then listed in the run summary's `partial_inputs`). Example: [the replies to a post, with nested conversations](https://apify.com/hyperbach/threads-scraper/examples/scrape-threads-post-replies-and-conversations).
- **Search** — A keyword in `keywords` reads the Top surface (about 20 posts, the page a visitor sees); `searchSurfaces` adds the Tags surface and `accounts` (the accounts Threads' search box offers for the keyword, up to 10, each an exact profile row charged as a profile); a `#tag` reads the Tags surface for that tag. Every search row names its keyword (`query`) and surface (`search_surface`). Threads shows a visitor one page per surface, no Recent sort and no date window: `list_note` says so on every post row, `keyword_match` says whether the keyword is in the text, the topic tag, the quoted post or the author, and `keywordMatchOnly` leaves the rest out before the charge. The page changes between loads: `searchPasses` loads it again on fresh exits and adds the posts not seen yet, each charged once (a second load added 43 posts to 58 for starbucks and #coffee; 6 loads added 5 to 21 for a rare word). Examples: [posts, tags and accounts for a keyword](https://apify.com/hyperbach/threads-scraper/examples/search-threads-posts-tags-and-accounts-for-a-keyword), [popular posts that mention a keyword](https://apify.com/hyperbach/threads-scraper/examples/threads-popular-posts-that-mention-a-keyword).
- **Views, authors, filters** — `includeViews` reads the view count of every profile post, search hit and reply (one small request per row, charged as `post_views`); `includeAuthorProfiles` adds one exact `profile` row per author and puts `author_followers` and `engagement_rate` on the post rows; `includeRaw` keeps Threads' own object on each row. `minLikes` and `verifiedOnly` leave list rows out before the charge; a post you name is always delivered. Example: [posts with view counts and engagement rate](https://apify.com/hyperbach/threads-scraper/examples/threads-posts-with-view-counts-and-engagement-rate).
- **A schedule** — Save a task with your profiles or keywords and `onlyNew`, and schedule it: each run reads the newest posts and delivers only those no earlier run of the task delivered; a post skipped this way is free, so a run with nothing new costs the profile rows only. The caps bound the depth read, so a daily run with `maxPostsPerProfile` 50 reads the same newest 50 every day. `stateStoreName` names the memory; with it, profile rows carry `followers_previous` and `followers_change`, and a post read again carries `engagement_change`. Example: [monitor accounts for new posts](https://apify.com/hyperbach/threads-scraper/examples/monitor-threads-accounts-for-new-posts).

### Input configuration

| field | type | default | what it does |
|---|---|---|---|
| `profiles` | `array` | `[]` | Threads profiles, one per line, in any form: `zuck`, `@zuck`, `https://www.threads.com/@zuck`, `https://www.threads.net/@zuck`, or a tab link such as `https://www.threads.com/@zuck/replies`. Each gives one `profile` row (the exact follower count, bio, links, tags, the shown threads count) and up to `maxPostsPerProfile` `post` rows from the tabs in `profileTabs`. The same user given several ways is one row. A value that is not a Threads profile comes back as a free row that says why. |
| `posts` | `array` | `[]` | Threads posts, one per line: `https://www.threads.com/@zuck/post/DdZ7sQvkTFn`, the same on threads.net, `https://www.threads.com/t/DdZ7sQvkTFn`, or the 11-character code `DdZ7sQvkTFn`. Each gives one `post` row with its five exact counts and its view count, and up to `maxRepliesPerPost` `post` rows of replies. One row per post: the same post given several ways is one row. A post that does not exist is a free row that says so. |
| `keywords` | `array` | `[]` | Search terms, one per line. A word or phrase reads the Top surface (about 20 posts, the page a visitor sees; `searchSurfaces` adds the Tags surface and the accounts the search box offers); `#tag` reads the Tags surface for that tag; a search link `https://www.threads.com/search?q=…&serp_type=tags` is read as given. Threads shows a visitor one page per surface, no Recent sort and no date window: every row says so (`list_note`) and whether it holds the keyword (`keyword_match`). |
| `maxPostsPerProfile` | `integer` | `25` | How many posts to read for each profile, newest first, from each tab in `profileTabs`; 0 = the profile row only. The Threads tab is read to the list's end (168 of 168 shown in our test, 1,400+ for a busy account); the replies and reposts tabs give their first page (about 30 posts, all Threads shows a visitor). With `onlyNew` or the filters, fewer rows are delivered than read: the cap bounds the depth, so a scheduled run reads the same newest posts every time. |
| `profileTabs` | `array` | `[]` | Which tabs of each profile to read for its posts; empty = `threads`. A tab given in a profile link (`…/@zuck/replies`) applies to that profile. Needs `maxPostsPerProfile` above 0. |
| `postedAfter` | `string` | `""` | A UTC date (`2026-10-01`) or time (`2026-10-01T12:00:00Z`): on the Threads tab, the walk stops at the first post older than this (pinned posts excepted), so a scheduled run reads only the recent posts. Empty = no date floor. Search has no date window (Threads shows a visitor none). |
| `includeProfileRow` | `boolean` | `true` | Deliver each profile's own row (exact followers, bio, links; charged as a profile) with its posts. Off: the posts only, so 10 posts asked are 10 charged rows (the posts still carry the author's follower count from the profile page). A profile asked with `maxPostsPerProfile` 0 always gives its row. |
| `maxRepliesPerPost` | `integer` | `0` | How many replies to read under each post in `posts`, as `post` rows with `reply_level`, `parent_post_code` and `root_post_code`; 0 = the post row only. Read to the list's end: Threads serves a visitor most but not all of a post's replies (231 + 33 nested of 318 in our test), and the post row says how many (`replies_note`). With the filters, fewer rows are delivered than read. |
| `repliesOrder` | `top` / `recent` | `"top"` | The order Threads lists the replies in. |
| `includeNestedReplies` | `boolean` | `false` | Also read the conversation under each reply that has replies (one request per such reply), as `post` rows with `reply_level` 2 and deeper. Counted in `maxRepliesPerPost`. |
| `searchSurfaces` | `array` | `[]` | The surfaces read for each plain keyword, one page each; empty = `top`. A `#tag` keyword always reads the Tags surface. A post found on both surfaces is one row. `accounts` gives the accounts Threads' search box offers for the keyword (up to 10), each as an exact profile row charged as a profile. |
| `searchPasses` | `integer` | `1` | How many times to load each post surface of a keyword. Threads gives a visitor one page with no cursor, but the page changes between loads: each pass after the first loads it again on a fresh exit and adds the posts not seen yet, charged once (a second pass added 43 posts to 58 for starbucks and #coffee; 6 passes added 5 to 21 for a rare word). 1 = the page a visitor sees. |
| `maxPostsPerKeyword` | `integer` | `0` | The most posts read per keyword over its post surfaces (0 = every post of the pages read: about 20 a surface), and the most accounts from the `accounts` surface. With `keywordMatchOnly`, `onlyNew` or the filters, fewer rows are delivered than read. |
| `keywordMatchOnly` | `boolean` | `false` | Deliver only the search rows whose text, topic tag, quoted post or author holds the keyword (`keyword_match` true). Threads' Top surface ranks related posts too (about half hold the word in our test); off, every row is delivered with its flag. |
| `includeViews` | `boolean` | `false` | Read the view count of every profile post, search hit and reply (one request per row: Threads serves views only on a post's own page). A post you name in `posts` carries its views at no extra cost. Charged per row that gets one (`post_views`). |
| `includeAuthorProfiles` | `boolean` | `false` | One exact `profile` row per distinct author of the posts delivered (the exact follower count; one small request per author), charged as a profile; the post rows then carry `author_followers` and `engagement_rate`. Every post row already carries the author's username, id, name, verification and picture. |
| `includeRaw` | `boolean` | `false` | Keep Threads' own JSON object on each row (`raw`), for fields this Actor does not name. |
| `minLikes` | `integer` | `0` | Leave out profile posts, search hits and replies with fewer likes than this; 0 = no floor. A post you name in `posts` is always delivered. |
| `verifiedOnly` | `boolean` | `false` | Keep only list rows whose author is verified. |
| `onlyNew` | `boolean` | `false` | Deliver only posts no earlier run of the same task delivered (profile tabs and search rows); turns the memory on. With the memory on, a profile row says how its followers moved since the last run and a post read again carries its engagement change. |
| `stateStoreName` | `string` | `""` | Name of the memory between runs. Given, it turns the memory on: profile rows get `followers_previous` and `followers_change`, post rows read again get `engagement_change`; with `onlyNew`, posts delivered before are skipped. Empty = derived from the inputs when `onlyNew` is on. |
| `maxItems` | `integer` | `0` | The most charged rows this run delivers, profiles, posts and replies together. 0 = no limit. Free rows (not found, invalid, empty, duplicate) do not count. |
| `proxyUrl` | `string` |  | `http://user:pass@host:port` — a proxy of yours to read Threads through, tried before the Actor's own exits. Leave empty: the Actor brings its own. |
| `runTag` | `string` | `""` | A label copied onto every row (`run_tag`), to tell runs apart in a shared dataset. |

### Errors

**A run does not fail because of your input.** When an input cannot be used — a date that is not a date, an id in the wrong form, two settings that contradict each other — the run ends **Succeeded**, charges nothing, and says what to change:

- the status message starts with `INPUT REJECTED`;
- the dataset holds one row, `{"error": true, "code": "…", "message": "…"}`, and no results;
- the key-value store record `ERROR` holds the same object.

From code, check `error` on the first row before reading results.

A search that matches nothing is not an error: the dataset is empty and nothing is charged. A run that ends **Failed** is a fault on our side, never your input; it charges nothing, and we are alerted.

### FAQ

**Can it read Recent search results, search by date, or the followers, following and likers of an account?**

No: these need a login. Threads' own page router strips the Recent filter and the date parameters from a visitor's search, and the follower, following and liker lists open only to a logged-in account; the actors that sell them use a login, their own or your session. This Actor does not log in or take a cookie, and the rows say what the source served (`list_note`). What a visitor does get: one page of Top and Tags results per keyword (more with `searchPasses`), the accounts the search box offers (`searchSurfaces` accounts), a profile's posts to the end of its list, and the replies to a post with their conversations.

**Are the counts exact?**

Yes: every count on every row is the exact integer Threads serves, equal to the counts a logged-in user sees. The view count is on every post you name (free) and on list rows with `includeViews`; where no count was read it is null, never 0. The profile's threads count is exact when Threads shows a plain number and null (with the shown text) when Threads rounds it.

**Why are fewer replies delivered than the post's reply count?**

Threads serves a visitor most but not all of a post's replies: on our reference post it served 250 of 318 (the rest are hidden, deleted or from private accounts). The post row's `replies_note` says how many were served and that the list ended; `includeNestedReplies` adds the conversations under each reply. When Threads rate-limited calls of the run, the note says so and the run summary lists the post under `partial_inputs`: a quieter read may serve more (in our test the same walk on a second exit served no reply the first had missed, 148 of 148).

**What is charged?**

Delivered rows: a profile, a post (a post you named, a profile's post, a search hit), a reply, and the `post_views` add-on on a list row whose view count was read for it. Rows for inputs that name nothing (not found, invalid, empty, duplicate), posts `onlyNew` skips or the filters leave out, and a run that failed before its first row are free; a run that stops early (a timeout, an abort) is charged for the rows it delivered. `maxItems` caps every charged row together.

**Does it need a proxy or a Threads account?**

No. The Actor brings its own exits (datacenter IPs are served) and reads Threads as a visitor; `proxyUrl` lets you put a proxy of your own in front.

**Can an AI agent use it (MCP)?**

Yes. Apify's MCP server (`https://mcp.apify.com`) offers Store Actors as tools to Claude, Cursor and other MCP clients: add this Actor and the agent sends `profiles`, `posts` or `keywords` and gets the rows back. The synchronous API (`run-sync-get-dataset-items`) does the same in one HTTP call.

### Integration

#### JavaScript

```javascript
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('hyperbach/threads-scraper').call({"profiles": ["zuck"], "maxPostsPerProfile": 25});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

#### Python

```python
from apify_client import ApifyClient
client = ApifyClient('YOUR_TOKEN')
run = client.actor('hyperbach/threads-scraper').call(run_input={'profiles': ['zuck'], 'maxPostsPerProfile': 25})
items = client.dataset(run['defaultDatasetId']).list_items().items
```

#### CLI

```bash
apify call hyperbach/threads-scraper --input '{"profiles": ["zuck"], "maxPostsPerProfile": 25}'
```

#### REST

```bash
curl -X POST "https://api.apify.com/v2/acts/hyperbach~threads-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H 'Content-Type: application/json' -d '{"profiles": ["zuck"], "maxPostsPerProfile": 25}'
```

### Support

apify@hyperbach.com

*This page is generated from the Actor's schemas and a live sample — it cannot describe a field the Actor does not have.*

# Actor input Schema

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

Threads profiles, one per line, in any form: `zuck`, `@zuck`, `https://www.threads.com/@zuck`, `https://www.threads.net/@zuck`, or a tab link such as `https://www.threads.com/@zuck/replies`. Each gives one `profile` row (the exact follower count, bio, links, tags, the shown threads count) and up to `maxPostsPerProfile` `post` rows from the tabs in `profileTabs`. The same user given several ways is one row. A value that is not a Threads profile comes back as a free row that says why.

## `posts` (type: `array`):

Threads posts, one per line: `https://www.threads.com/@zuck/post/DdZ7sQvkTFn`, the same on threads.net, `https://www.threads.com/t/DdZ7sQvkTFn`, or the 11-character code `DdZ7sQvkTFn`. Each gives one `post` row with its five exact counts and its view count, and up to `maxRepliesPerPost` `post` rows of replies. One row per post: the same post given several ways is one row. A post that does not exist is a free row that says so.

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

Search terms, one per line. A word or phrase reads the Top surface (about 20 posts, the page a visitor sees; `searchSurfaces` adds the Tags surface and the accounts the search box offers); `#tag` reads the Tags surface for that tag; a search link `https://www.threads.com/search?q=…&serp_type=tags` is read as given. Threads shows a visitor one page per surface, no Recent sort and no date window: every row says so (`list_note`) and whether it holds the keyword (`keyword_match`).

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

How many posts to read for each profile, newest first, from each tab in `profileTabs`; 0 = the profile row only. The Threads tab is read to the list's end (168 of 168 shown in our test, 1,400+ for a busy account); the replies and reposts tabs give their first page (about 30 posts, all Threads shows a visitor). With `onlyNew` or the filters, fewer rows are delivered than read: the cap bounds the depth, so a scheduled run reads the same newest posts every time.

## `profileTabs` (type: `array`):

Which tabs of each profile to read for its posts; empty = `threads`. A tab given in a profile link (`…/@zuck/replies`) applies to that profile. Needs `maxPostsPerProfile` above 0.

## `postedAfter` (type: `string`):

A UTC date (`2026-10-01`) or time (`2026-10-01T12:00:00Z`): on the Threads tab, the walk stops at the first post older than this (pinned posts excepted), so a scheduled run reads only the recent posts. Empty = no date floor. Search has no date window (Threads shows a visitor none).

## `includeProfileRow` (type: `boolean`):

Deliver each profile's own row (exact followers, bio, links; charged as a profile) with its posts. Off: the posts only, so 10 posts asked are 10 charged rows (the posts still carry the author's follower count from the profile page). A profile asked with `maxPostsPerProfile` 0 always gives its row.

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

How many replies to read under each post in `posts`, as `post` rows with `reply_level`, `parent_post_code` and `root_post_code`; 0 = the post row only. Read to the list's end: Threads serves a visitor most but not all of a post's replies (231 + 33 nested of 318 in our test), and the post row says how many (`replies_note`). With the filters, fewer rows are delivered than read.

## `repliesOrder` (type: `string`):

The order Threads lists the replies in.

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

Also read the conversation under each reply that has replies (one request per such reply), as `post` rows with `reply_level` 2 and deeper. Counted in `maxRepliesPerPost`.

## `searchSurfaces` (type: `array`):

The surfaces read for each plain keyword, one page each; empty = `top`. A `#tag` keyword always reads the Tags surface. A post found on both surfaces is one row. `accounts` gives the accounts Threads' search box offers for the keyword (up to 10), each as an exact profile row charged as a profile.

## `searchPasses` (type: `integer`):

How many times to load each post surface of a keyword. Threads gives a visitor one page with no cursor, but the page changes between loads: each pass after the first loads it again on a fresh exit and adds the posts not seen yet, charged once (a second pass added 43 posts to 58 for starbucks and #coffee; 6 passes added 5 to 21 for a rare word). 1 = the page a visitor sees.

## `maxPostsPerKeyword` (type: `integer`):

The most posts read per keyword over its post surfaces (0 = every post of the pages read: about 20 a surface), and the most accounts from the `accounts` surface. With `keywordMatchOnly`, `onlyNew` or the filters, fewer rows are delivered than read.

## `keywordMatchOnly` (type: `boolean`):

Deliver only the search rows whose text, topic tag, quoted post or author holds the keyword (`keyword_match` true). Threads' Top surface ranks related posts too (about half hold the word in our test); off, every row is delivered with its flag.

## `includeViews` (type: `boolean`):

Read the view count of every profile post, search hit and reply (one request per row: Threads serves views only on a post's own page). A post you name in `posts` carries its views at no extra cost. Charged per row that gets one (`post_views`).

## `includeAuthorProfiles` (type: `boolean`):

One exact `profile` row per distinct author of the posts delivered (the exact follower count; one small request per author), charged as a profile; the post rows then carry `author_followers` and `engagement_rate`. Every post row already carries the author's username, id, name, verification and picture.

## `includeRaw` (type: `boolean`):

Keep Threads' own JSON object on each row (`raw`), for fields this Actor does not name.

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

Leave out profile posts, search hits and replies with fewer likes than this; 0 = no floor. A post you name in `posts` is always delivered.

## `verifiedOnly` (type: `boolean`):

Keep only list rows whose author is verified.

## `onlyNew` (type: `boolean`):

Deliver only posts no earlier run of the same task delivered (profile tabs and search rows); turns the memory on. With the memory on, a profile row says how its followers moved since the last run and a post read again carries its engagement change.

## `stateStoreName` (type: `string`):

Name of the memory between runs. Given, it turns the memory on: profile rows get `followers_previous` and `followers_change`, post rows read again get `engagement_change`; with `onlyNew`, posts delivered before are skipped. Empty = derived from the inputs when `onlyNew` is on.

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

The most charged rows this run delivers, profiles, posts and replies together. 0 = no limit. Free rows (not found, invalid, empty, duplicate) do not count.

## `proxyUrl` (type: `string`):

`http://user:pass@host:port` — a proxy of yours to read Threads through, tried before the Actor's own exits. Leave empty: the Actor brings its own.

## `runTag` (type: `string`):

A label copied onto every row (`run_tag`), to tell runs apart in a shared dataset.

## Actor input object example

```json
{
  "profiles": [
    "zuck",
    "https://www.threads.com/@threads"
  ],
  "posts": [
    "https://www.threads.com/@zuck/post/DdZ7sQvkTFn"
  ],
  "keywords": [
    "superintelligence"
  ],
  "maxPostsPerProfile": 25,
  "profileTabs": [],
  "postedAfter": "",
  "includeProfileRow": true,
  "maxRepliesPerPost": 0,
  "repliesOrder": "top",
  "includeNestedReplies": false,
  "searchSurfaces": [],
  "searchPasses": 1,
  "maxPostsPerKeyword": 0,
  "keywordMatchOnly": false,
  "includeViews": false,
  "includeAuthorProfiles": false,
  "includeRaw": false,
  "minLikes": 0,
  "verifiedOnly": false,
  "onlyNew": false,
  "stateStoreName": "",
  "maxItems": 0,
  "runTag": ""
}
```

# Actor output Schema

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

All scraped records in the default dataset. Two row types share one dataset (`row_type` says which): `post` (a post, a reply, a search hit) and `profile`:

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

How the run went: rows charged per event, free rows by status, a report line per input with its stop reason, posts skipped or filtered, and the source counters (requests, exits, bytes, refusals).

# 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": [
        "zuck",
        "https://www.threads.com/@threads"
    ],
    "posts": [
        "https://www.threads.com/@zuck/post/DdZ7sQvkTFn"
    ],
    "keywords": [
        "superintelligence"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("hyperbach/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 = {
    "profiles": [
        "zuck",
        "https://www.threads.com/@threads",
    ],
    "posts": ["https://www.threads.com/@zuck/post/DdZ7sQvkTFn"],
    "keywords": ["superintelligence"],
}

# Run the Actor and wait for it to finish
run = client.actor("hyperbach/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 '{
  "profiles": [
    "zuck",
    "https://www.threads.com/@threads"
  ],
  "posts": [
    "https://www.threads.com/@zuck/post/DdZ7sQvkTFn"
  ],
  "keywords": [
    "superintelligence"
  ]
}' |
apify call hyperbach/threads-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,hyperbach/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/IvGB0b6RUX5uBEyYz/builds/utkN0iBQhIo3WPb4K/openapi.json
