# Threads Scraper — Instagram Threads API, Posts, Replies, Videos (`steadyfetch/threads-scraper`) Actor

Instagram Threads posts by handle: the latest posts as rows — text, date, likes, replies, reposts, quotes, image and video links — or paste a post link for its exact counts and top replies. Optional profile row: followers, bio, links. No login, no start fee. Threads shows 15 posts per profile.

- **URL**: https://apify.com/steadyfetch/threads-scraper.md
- **Developed by:** [Steadyfetch Team](https://apify.com/steadyfetch) (community)
- **Categories:** Social media, Automation, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% 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?

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 — Instagram Threads API, Posts, Replies, Videos

**Type a handle and get its posts. Paste a post link and get that post with its replies.** One JSON row per post: the post ID and its short code, the permalink, the handle and the stable numeric author ID, the post text, the moment it was published, the media type, the like, reply, repost, quote and reshare counts, the links and mentions pulled out of the text, an image link, a playable video link and the moment that link expires. Paste a post link instead and the row also carries that post's **view count** and its **top replies**. **From $1.00 per 1,000 posts** on the Business plan ($3.00 on the Apify free plan), platform usage included, no start fee. You are charged only when a row lands in your dataset: a handle Threads will not show, a profile that has published nothing, and a post link that is gone all cost $0.

**Using an AI agent?** Pin this actor in Apify's MCP server with one link: `https://mcp.apify.com?tools=steadyfetch/threads-scraper`

- **Actor id:** `steadyfetch/threads-scraper`
- **Input:** `{ "handles": ["natgeo"] }` — the one field you have to set. Or `{ "postUrls": ["https://www.threads.com/@zuck/post/ABC123"] }` for individual posts.
- **Cap the bill:** set `maxTotalChargeUsd` on the run (a run option, not Actor input), e.g. `0.50` — the run stops when it reaches it.
- **Your cap is the cap.** `resultsLimit` (default 15) caps each handle and `maxItems` (default 500) caps the whole run. Neither is ever overshot. If your input carries `limit`, `count` or `max_posts` instead — the names other Threads scrapers use — they are read as the right one of the two and an uncharged note row says which.

**Threads shows the last 15 posts of a profile to a logged-out reader, and there is no way to page further.** That is Threads' ceiling, not ours: measured on four accounts on 11 September 2026, every one of them returned exactly 15 with no continuation token, whatever the account's posting rate. Ask for 30 and you get 15 plus one uncharged row per handle saying so. Nobody selling this data can do better without a login, and an actor that bills you for 30 rows to deliver 14 unique ones is the complaint this listing was built against.

**Just want to see it work?** Click **Start** with nothing set and the run returns built-in sample rows — the exact output shape, read from nothing, at no result fee.

### What you get

| column | what it is |
|---|---|
| `postId` · `code` · `url` | the post's stable numeric ID, its short code, and its permalink |
| `handle` · `authorId` · `isVerified` | the author's handle, the stable numeric ID behind it, and whether the account is verified. A Threads handle is the Instagram handle and can be changed by its owner; `authorId` cannot. |
| `text` | the post's own text, exactly as published |
| `createdAt` | when it was published, ISO-8601 UTC |
| `mediaType` | `text`, `image`, `video` or `carousel` |
| `likeCount` · `replyCount` · `repostCount` · `quoteCount` · `reshareCount` | the five engagement counts. Real numbers, not zeros: a measured post carries 18,150 likes, 4,742 replies, 249 reposts and 144 quotes. An account that hides its counts gets `countsHidden: true` and explicit nulls — never a fake `0`. |
| `viewCount` | views, **on a pasted post link only**. A profile listing does not carry it and the column is an explicit `null` there. |
| `isReply` · `isPinned` · `isPaidPartnership` | flags read off the post itself |
| `links` · `mentions` | every link and every @mention inside the text, pulled out for you |
| `imageUrl` · `videoUrl` · `videoUrlExpiresAt` | the media. **Video links expire within about a day and a half** and image links within about five days — `videoUrlExpiresAt` is the exact moment, decoded from the link itself, so you can fetch or re-run before it. |
| `carousel` | a carousel post's items, each with its own media type, image link and video link |
| `repliesTotal` · `replies` | on a pasted post link: **a pasted post returns its top replies (about twenty) inside the row; there is no page 2 of replies.** `repliesTotal` is how many exist, so a measured row reads "22 of 4,742" and you always know what you are looking at. |
| `charged` · `chargedEvents` | whether this row was billed, and by which event |
| `status` · `statusReason` | what happened, in a sentence, on every row including the ones that cost nothing |

A `rowType: profile` row — switched on with **Include a profile row per handle** — carries `handle`, `id`, `fullName`, `biography`, `followerCount`, `isVerified`, `bioLinks` and `profilePicUrl`.

### What it costs

Three charged events. Nothing is charged for starting a run, and nothing is charged for a row that did not land.

| event | what one unit is | Apify free plan | paid plans, from |
|---|---|---|---|
| **Post** | one post row from a handle's listing | $0.003 | $0.001 |
| **Profile** | one profile row | $0.012 | $0.005 |
| **Post lookup** | one pasted post link, with its view count and its top replies | $0.012 | $0.005 |

Three handles at 15 posts each is 45 post rows: about **$0.135** on the Apify free plan, **$0.045** on the Business plan. A pasted post link costs one Post lookup whatever its replies contain — the replies ride along inside the row and are not charged separately.

**Never charged:** a handle Threads answers does not exist or is private, a profile that has published nothing, a post link Threads answers is gone, a read that failed on our side, a post your own date window dropped, a post this account already has, and every row a limit or the run clock stopped us from collecting. Each of those is still delivered as a row, labelled, with a sentence saying which it was.

### Re-running the same handles

Every run remembers the posts it delivered to your account, in the key-value store `threads-account` in your own Apify account. A later run skips them — not delivered again, not charged — so a daily schedule on the same handles costs only what is new. Switch **Include posts you already have** on to get them anyway, marked `repeat: true` and still uncharged. The memory is a key-value store in your own Apify account, so a run started with a scoped API token in restricted-access mode needs key-value store Read, Write and Create permission, or Actor runs set to Full access (Write is what lets the run remember what it delivered; without it every run pays again); without it the run says so on its status line and cannot skip what you already have.

### If something is wrong

This actor reads a public surface, and a public surface can change. When it does, failed items are never charged and the row says so in words rather than leaving you to guess from an empty dataset.

Open the **Issues** tab on this actor with your run ID and the input you used, and it gets looked at. If it earned its keep, a rating helps other buyers find it, and saving the actor keeps it one click away.

### Related actors

- A Threads **video** you want the spoken words of: the `videoUrl` column is a direct MP4 link — hand it to `steadyfetch/media-transcriber` in its `urls` field while the link is still fresh, and it transcribes it by the minute. Pass the `videoUrl`, not the threads.com page link.
- Instagram profiles and their posts: `steadyfetch/instagram-profile-posts`.
- Instagram reels with their transcripts: `steadyfetch/instagram-reel-transcript-scraper`.

# Actor input Schema

## `handles` (type: `array`):

Threads handles, one per line — "natgeo", "@natgeo" or a profile link (https://www.threads.com/@natgeo). Threads shows the last 15 posts of a profile to a logged-out reader, so 15 is the most any handle can return and a bigger limit delivers 15 and says so, uncharged. Leave this empty and the run returns built-in sample rows instead of collecting anything, so you can see the output shape at no result fee. A post link put here is routed to the post box automatically, not refused.

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

Individual Threads post links, one per line — https://www.threads.com/@zuck/post/ABC123 (threads.net links are read too). A pasted post link is a first-class input here, not a workaround: each one returns its own row carrying the post's view count and its top replies (about twenty, as Threads serves them — there is no page 2 of replies), which a profile listing does not have. Charged as one "Post lookup" when the row lands; a link Threads answers is gone costs nothing.

## `startUrls` (type: `array`):

One list for both kinds of link, for buyers moving over from another scraper. A profile link or bare handle lists that profile's posts; a post link returns that one post with its replies. Each line is sorted into the right door before anything is bought, so a post link never costs a profile read and a profile link never costs a lookup.

## `includeProfile` (type: `boolean`):

OFF (default): only post rows. ON: each handle also gets one `rowType: profile` row with `followerCount`, `fullName`, `biography`, `bioLinks`, `isVerified` and `profilePicUrl`, at $0.005 on the Business plan ($0.012 on the Apify free plan) on top of the post rows.

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

Hard ceiling for the WHOLE run — it never delivers or bills more rows than this, across every handle, every post link and the profile rows, whatever else is set. Other scrapers call this `limit` or `count`; those names are read as this field too and the smallest of them wins, with one uncharged note row saying which was read. The per-handle cap is the separate "Max posts per handle" below. Accepted range 1 to 2,000; a number outside it is moved to the nearest value in range and one uncharged note row says so, rather than the run being refused.

## `resultsLimit` (type: `integer`):

How many posts to take from each handle, at most 15 — Threads publishes no more than that to a logged-out reader and there is no way to page further. Set 30 and the run delivers 15 and adds one uncharged note per handle saying the cap is Threads', not ours. Set 5 and you get exactly 5. Other Threads scrapers call this `max_posts` or `maxPosts`; both names are read as this field. Accepted range 1 to 1,000 — higher than the 15 Threads serves on purpose, so the ask you typed is the ask this run reports against.

## `newerThan` (type: `string`):

A date, YYYY-MM-DD, or a relative window like "7 days". Posts published before it are dropped, uncharged, and the status line says how many.

## `olderThan` (type: `string`):

A date, YYYY-MM-DD, or a relative window like "30 days". Posts published after it are dropped, uncharged, and counted on the status line.

## `maxRunSeconds` (type: `integer`):

The run stops cleanly before this many seconds and reports what is left, instead of being killed by a timeout. A time limit ends the collecting, never the delivering: rows already in hand are always written out. Accepted range 30 to 3,600 seconds; a number outside it is moved to the nearest value in range and one uncharged note row says so, rather than the run being refused.

## `includeSeen` (type: `boolean`):

OFF (default): a post already delivered to your account is skipped — no row, no charge. ON: those posts are delivered again anyway, marked `repeat: true`, and still not charged. Posts older than 90 days in your account's memory count as new again either way.

## `resumeFromDatasetId` (type: `string`):

The dataset ID of a previous run of this actor. Posts already in it are treated as posts you already have and are skipped, so a follow-up run collects only what is new even from a different Apify account. Selecting it here is what grants this run permission to read it.

## Actor input object example

```json
{
  "handles": [],
  "postUrls": [],
  "startUrls": [],
  "includeProfile": false,
  "maxItems": 500,
  "resultsLimit": 15,
  "maxRunSeconds": 900,
  "includeSeen": false
}
```

# Actor output Schema

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

One row per post delivered, with the same columns whichever door it came through: post ID, short code, permalink, handle and stable author ID, the post text, the date it was published, the media type (text, image, video or carousel), the like, reply, repost, quote and reshare counts, links and mentions pulled out of the text, an image link, a signed video link with the moment it expires, and the carousel's items folded into one column. A pasted post link also carries the post's view count and its top replies (about twenty, as Threads serves them) with `repliesTotal` saying how many exist in total, so the row reads "21 of 4,742". Three events, one flag: `charged` is the row itself and `chargedEvents` names which event paid for it — `post` on a profile listing, `post-lookup` on a pasted link, `profile-lookup` on a profile row. An account that hides its counts gets `countsHidden: true` and explicit nulls, never a fake zero.

## `profiles` (type: `string`):

One `rowType: profile` row per handle when "Include a profile row per handle" is on: handle, stable profile ID, full name, biography, follower count, verification, bio links and the profile picture. Charged as one `profile-lookup` each; a handle Threads does not show is an uncharged miss instead.

## `misses` (type: `string`):

One uncharged row for every handle or link that returned less than it was asked for, saying which happened: `profile_not_found_or_private` (Threads answered that the profile does not exist or is private — its own message says both and cannot tell them apart), `profile_no_posts` (the profile is there and has published none we can reach), `post_not_found` (Threads answered that the post is gone), `feed_unavailable` (the read failed on our side — a re-run is the fix), `capped_by_threads` (a note: you asked for more than the 15 posts Threads publishes), `stopped_at_limit` (one of your own limits or the run clock stopped it first), `date_filtered` (your own date window dropped it) and `vendor_budget` (this actor reached its own monthly collection allowance). None of these is charged.

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

Rows delivered by door, what was asked for, posts your account already had, posts your date window dropped, what stopped the run, and the charged-event totals for `post`, `profile-lookup` and `post-lookup`.

# 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 = {
    "handles": [],
    "postUrls": [],
    "startUrls": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadyfetch/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 = {
    "handles": [],
    "postUrls": [],
    "startUrls": [],
}

# Run the Actor and wait for it to finish
run = client.actor("steadyfetch/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 '{
  "handles": [],
  "postUrls": [],
  "startUrls": []
}' |
apify call steadyfetch/threads-scraper --silent --output-dataset

```

## MCP server setup

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