# Threads Scraper - Profiles, Posts & Keyword Search, No Login (`lanternlab318/threads`) Actor

Get public Threads data by username, post URL or keyword: profile name, bio, exact follower count and verified badge, plus post text, publish time, likes, replies, reposts, quotes and media URLs. No login or cookies. Deduplicated; incomplete inputs are marked partial, never shown as empty.

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

## Pricing

$1.49 / 1,000 profile or posts

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

## Threads Scraper - Profiles, Posts & Keyword Search

Give it Threads usernames, post URLs or keywords and get the public data back: for a profile its name, bio, follower count and verified badge; for every post its text, publish time, likes, replies, reposts, quotes, media URLs and post ID. No Threads or Instagram login, no cookies, no account of your own. **$1.49 per 1,000 rows, no start fee.** See [Pricing](#pricing).

Input:

```json
{
  "usernames": ["zuck", "https://www.threads.com/@natgeo"],
  "postUrls": ["https://www.threads.com/@zuck/post/DeZtp-TkQHh"],
  "searchQueries": ["apify"],
  "maxPostsPerProfile": 15
}
```

Output - one row per post (`recordType: "post"`) and one per profile (`recordType: "profile"`); per-input status rows and the run summary are free. Sample from a real run:

```json
{
  "recordType": "post",
  "postId": "4006434140014576097",
  "postCode": "DeZtp-TkQHh",
  "url": "https://www.threads.com/@zuck/post/DeZtp-TkQHh",
  "text": "Introducing Muse Wonder, our new family of frontier models…",
  "publishedAt": "2026-10-12T16:56:29.000Z",
  "likeCount": 2112,
  "replyCount": 527,
  "repostCount": 168,
  "quoteCount": 60,
  "mediaUrls": ["https://scontent-kul2-2.cdninstagram.com/v/t51.82787-15/844965867…"],
  "authorUsername": "zuck",
  "authorUserId": "63055343223",
  "threadRootId": "4006434140014576097",
  "parentPostId": null,
  "isThreadRoot": true,
  "inputKind": "profile",
  "inputValue": "zuck",
  "rank": 1,
  "partial": false,
  "partialReason": null,
  "collectedAt": "2026-10-24T03:21:44.108Z"
}
```

```json
{
  "recordType": "profile",
  "userId": "63055343223",
  "username": "zuck",
  "fullName": "Mark Zuckerberg",
  "bio": "Mostly superintelligence and MMA takes",
  "followerCount": 5753638,
  "isVerified": true,
  "isPrivate": false,
  "profileUrl": "https://www.threads.com/@zuck",
  "postsCollected": 33,
  "partial": false,
  "collectedAt": "2026-10-24T03:21:44.108Z"
}
```

### What you can and cannot get

| | |
| --- | --- |
| Profiles | `username`, `fullName`, `bio`, `followerCount`, `isVerified`, `isPrivate`, `profilePicUrl`, `profileUrl`, `userId` |
| Posts | `postId`, `postCode`, `url`, `text`, `publishedAt`, `likeCount`, `replyCount`, `repostCount`, `quoteCount`, `mediaUrls`, `authorUsername`, `authorUserId` |
| Thread structure | `threadRootId` and `parentPostId` on every post, so an author's own multi-post thread stays readable in order. `parentPostId` is null on the post that starts a thread. |
| Not collected | Replies written by other accounts under a post. Followers and following lists. Anything behind a login, a private account or an age restriction. Email addresses, phone numbers and other personal contact details are not read even when a page carries them. |

#### Limits, stated up front

- **About 15 threads per profile and per keyword.** Threads renders a fixed window of the newest threads into a public profile or search page and offers no next page there without an account. One profile page measured 2026-10-24 listed 15 threads and 33 posts counting the author's own continuations of them. Ask for more with `maxPostsPerProfile` and the run writes everything the page carried and marks that profile `partial` with `partialReason: "profile_page_limit"` - a short list is never passed off as a short profile.
- **No view count.** Threads does not put it on the public page, so the field is not invented; likes, replies, reposts and quotes are all there.
- **No reply collection.** Replies under a post are out of scope for this Actor.
- **Follower counts are exact, not the rounded "5.8M" the page shows.**

### Rows that cost nothing

Every input - each username, post URL and keyword - also gets one free `input_status` row saying what it returned and whether that is all there was, and the run ends with a free `RUN_SUMMARY` row. Only `profile` and `post` rows are charged, and a post that matched several of your inputs is written and charged once.

That is what keeps an incomplete run honest:

| `partialReason` | What happened |
| --- | --- |
| `null` | The input returned everything its public page carried. `postsReturned: 0` with no reason is a real empty result - a keyword that matched nothing renders an empty result list, which is how it is told apart from a page that carried no data. |
| `no_data_in_page` | Threads answered with its logged-out page, which carries no data. The same answer comes back for a name that does not exist, a private account and a screened request, so this is never reported as zero posts. |
| `profile_page_limit` | More posts were asked for than the public profile page lists. Everything on the page was collected. |
| `post_not_on_page` | The page for a post URL did not carry that post. A deleted post, an author who went private and a post Threads withheld all answer the same way, so it is not reported as a post with no content. |
| `blocked` | Threads refused the request (rate limit or challenge page). Nothing was collected for that input; it does not mean there are no posts. |
| `upstream_request_cap` | The run reached `maxUpstreamRequests`. |
| `charge_limit_reached` | The maximum charge set for the run was reached. |
| `upstream_error` | A request failed in a way retrying inside the run would not fix. |
| `not_collected` | The run stopped earlier and never reached this input. |
| `unexpected_page_shape` | The page was read but did not carry the data in the shape this Actor knows. |

A run that collected nothing because every page was refused or withheld **fails**; a run that legitimately found nothing succeeds with zero charged rows.

### Pricing

Pay per result: **$1.49 per 1,000 rows**, no start fee, no monthly fee. A row is one `profile` or one `post`. Status rows, the run summary, duplicates and anything an input could not return are free, so a blocked or empty input costs nothing.

### Input reference

| Field | Default | Notes |
| --- | --- | --- |
| `usernames` | — | `zuck`, `@zuck` or a profile URL. Up to 500. |
| `postUrls` | — | `/@user/post/<code>` or `/t/<code>`, on threads.com or threads.net. Up to 500. |
| `searchQueries` | — | One keyword or phrase per entry. Up to 500. |
| `maxPostsPerProfile` | `15` | Upper bound on post rows per profile. Max 200, but see the limit above. |
| `maxUpstreamRequests` | derived | Hard ceiling on requests to Threads: one per profile, post URL and keyword. Derived from your input when left empty, so a run is never unbounded. |
| `proxyConfiguration` | Apify Proxy | Datacenter is what this Actor is measured on. Switch to residential if rows come back `blocked`. |

At least one of `usernames`, `postUrls` or `searchQueries` is required. Entries that are not a Threads username, post URL or keyword are reported in the run summary as `unusableInputs` and skipped; they do not fail the run. Repeated entries are collapsed before the first request.

### How it works

One HTTP GET per input against the public page, with no account, token or cookie: `threads.com` server-renders its public profile, post and search pages for indexing, and that rendering carries the page's own data as JSON inside the HTML. The Actor reads that JSON and writes the fields above; a field Threads did not send is `null`, never guessed.

# Actor input Schema

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

One profile per line. "zuck", "@zuck" and "https://www.threads.com/@zuck" are all accepted. Each one returns a profile row plus the posts its public profile page lists. Threads renders a fixed window of the newest threads into that page and offers no next page there without an account, so about 15 threads per profile is what a profile can return - the run says per profile how many the page offered and marks the profile partial when you asked for more.

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

One post URL per line, in either public form: https://www.threads.com/@user/post/<code> or https://www.threads.com/t/<code>. Each one returns that post only. Replies written under it are out of scope and are not collected.

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

One keyword or phrase per line. Each one returns the public posts Threads' own search page lists for it - about 15 threads per keyword, which is the whole window that page offers without an account. Use it to find posts about a topic; use usernames when you already know whose profile you want.

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

Upper bound on the post rows one profile may produce. The default, 15, is what a public profile page lists. A higher value does not reach more posts: Threads offers no next page on a public profile, so the row count is then Threads' ceiling, not this setting, and the run marks that profile partial with partialReason "profile_page_limit" so a short list is never mistaken for a short profile.

## `maxUpstreamRequests` (type: `integer`):

Hard ceiling on the HTTP requests this run sends to Threads - one per profile, post URL and keyword. At the ceiling collection stops: the inputs not reached are written with partialReason "upstream_request_cap" and the run then ends normally. Leave it empty and the Actor works a ceiling out from your own inputs, so a run is never unbounded.

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

The proxy the requests to Threads go through. The default (Apify Proxy, datacenter) is the cheapest and is what this Actor is measured on; Threads serves its public pages over it. Switch to residential if you see rows marked partialReason "blocked".

## Actor input object example

```json
{
  "usernames": [
    "zuck"
  ],
  "maxPostsPerProfile": 15,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

No description

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

No description

# 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 = {
    "usernames": [
        "zuck"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("lanternlab318/threads").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 = { "usernames": ["zuck"] }

# Run the Actor and wait for it to finish
run = client.actor("lanternlab318/threads").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 '{
  "usernames": [
    "zuck"
  ]
}' |
apify call lanternlab318/threads --silent --output-dataset

```

## MCP server setup

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

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/YVl8CWa4IiA2Dy8E9/builds/3Ty6aK6lbdm8MhzuL/openapi.json
