# Meta Threads Scraper (`scrapelabmax/meta-threads-scraper`) Actor

Collect public Meta Threads posts, profiles and search results without login. Stable IDs, UTC timestamps, honest nulls and per-input diagnostics.

- **URL**: https://apify.com/scrapelabmax/meta-threads-scraper.md
- **Developed by:** [Scrapelab Max](https://apify.com/scrapelabmax) (community)
- **Security:** Limited permissions
- **Stats:** 3 total users, 2 monthly users, 87.5% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 results

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

## Meta Threads Scraper

Collect **public** Meta Threads posts, profiles and search results — no login, no cookies, no browser. Every row has a stable numeric ID, a canonical URL, an ISO-8601 UTC timestamp and honest `null`s where Threads hides a number. Every run ends with a free per-input summary that tells you exactly what was requested, what came back, and why anything is missing.

- **Price:** $1.50 per 1,000 results ($0.0015 per emitted unique post or profile row). Failed inputs, duplicates, filtered rows and diagnostics are never charged.
- **Modes:** user posts, user replies, user reposts, keyword/hashtag post search, profile search, post URL lookup.
- **Honest limits:** Threads exposes at most 20 search results per keyword, about 13 profiles per profile search, a single page of replies/reposts, and no view counts to logged-out visitors. We report these in the output instead of hiding them. See [Limitations](#limitations).

### Table of contents

1. [Quick start](#quick-start)
2. [Modes and examples](#modes-and-examples)
3. [Input reference](#input-reference)
4. [Output](#output)
5. [Run summary and statuses](#run-summary-and-statuses)
6. [Pricing](#pricing)
7. [Limitations](#limitations)
8. [FAQ and troubleshooting](#faq-and-troubleshooting)
9. [Compliance](#compliance)
10. [Running locally and contributing](#running-locally-and-contributing)

### Quick start

1. Pick a **Mode**.
2. Fill in the one list that mode uses: `usernames`, `keywords` or `postUrls`.
3. Optionally set limits and filters. The defaults (50 rows per input, 1,000 per run) keep a first run cheap.
4. Run. Rows land in the default dataset; the diagnostic summary is in the key-value store under `RUN_SUMMARY`.

Minimal input:

```json
{
    "mode": "user_posts",
    "usernames": ["zuck"],
    "maxResultsPerInput": 20
}
```

### Modes and examples

| Mode | Reads | Emits | Pagination |
|---|---|---|---|
| `user_posts` | `usernames` | The account's own posts from its Threads tab (+ optional profile row) | Yes, until `maxResultsPerInput` / `maxRequestsPerInput` / end of feed |
| `user_replies` | `usernames` | Replies the account posted | **First public page only** (about 4 to 10 items) |
| `user_reposts` | `usernames` | Posts the account reposted | **First public page only** |
| `search_posts` | `keywords` | Posts matching each keyword or `#hashtag`, Top or Recent | **At most 20 per keyword and sort order** |
| `search_profiles` | `keywords` | Public profiles matching each keyword | **About 13 per keyword** |
| `post_urls` | `postUrls` | The linked post, its author's own continuation posts and (optionally) direct replies | Replies paginate under the same caps |

#### User posts

```json
{
    "mode": "user_posts",
    "usernames": ["zuck", "@mosseri", "https://www.threads.com/@instagram"],
    "maxResultsPerInput": 100,
    "includeProfiles": true,
    "since": "2026-01-01"
}
```

Handles, `@handles` and profile URLs are all accepted and normalized to a lowercase handle. With `includeProfiles: true` you also get one `profile` row per account **before** its posts; that row is billed separately and does not count toward `maxResultsPerInput`.

#### User replies and reposts

```json
{ "mode": "user_replies", "usernames": ["zuck"] }
```

```json
{ "mode": "user_reposts", "usernames": ["zuck"] }
```

Logged-out Threads serves a single page for these tabs and offers no cursor. Each input finishes with the warning `Logged-out access exposes only the first page …` so the gap is visible in `RUN_SUMMARY`. Reposted rows carry `reposted_post_id` and `source_tab: "reposts"`.

#### Keyword and hashtag search

```json
{
    "mode": "search_posts",
    "keywords": ["open source", "#threadsapi"],
    "sort": "recent",
    "minLikes": 5,
    "maxResultsPerInput": 20
}
```

Each keyword is one input. A post returned by several keywords is written **once**, with every keyword in `matched_keywords`, and billed once. If you ask for more than 20 per keyword the input still succeeds but carries the warning `Public search exposes at most 20 results per keyword`.

#### Profile search

```json
{ "mode": "search_profiles", "keywords": ["ux designer", "indie hacker"] }
```

Returns `profile` rows (username, name, bio, follower count when public, bio links). No contact enrichment: if Threads does not show it, it is `null`.

#### Post URLs

```json
{
    "mode": "post_urls",
    "postUrls": [
        "https://www.threads.com/@zuck/post/CuXFPIeLLod",
        "https://www.threads.net/@zuck/post/CuXFPIeLLod/media"
    ],
    "includeReplies": true,
    "maxResultsPerInput": 50
}
```

`threads.com`, legacy `threads.net` and `/media` deep links resolve to the same canonical post, and equal posts are deduplicated within the run. The target post and the author's own thread continuation have `source_tab: "post"`; replies have `source_tab: "post_replies"` and `parent_post_id`. Nested reply trees are not followed.

### Input reference

| Field | Type | Default | Notes |
|---|---|---|---|
| `mode` | enum | `user_posts` | See table above |
| `usernames` | string\[] | — | User modes. Handle, `@handle` or profile URL |
| `keywords` | string\[] | — | Search modes. One input per keyword |
| `postUrls` | string\[] | — | `post_urls` mode. Threads hosts only |
| `sort` | `top` | `recent` | `top` | `search_posts` only |
| `maxResultsPerInput` | 1–1000 | 50 | Rows accepted per input before pagination stops. Profile rows excluded |
| `maxTotalResults` | ≥1 | 1000 | Global cap; remaining inputs are not started once reached (`budget_reached`) |
| `maxRequestsPerInput` | 1–200 | 30 | Page fetches per input including the first |
| `since` / `until` | ISO date (UTC) | — | Inclusive bounds on `created_at`; applied before billing |
| `minLikes` | ≥0 | — | Posts with hidden like counts are dropped when set |
| `includeProfiles` | boolean | false | User modes: emit a billable `profile` row per account |
| `includeReplies` | boolean | false | `post_urls`: emit and paginate direct replies |
| `onlyNew` / `monitorName` | boolean / string | false / — | Beta repeat-run dedup, see [docs/monitoring.md](docs/monitoring.md) |
| `proxyConfiguration` | object | Apify Proxy | Keep enabled to avoid `rate_limited` |

Input is validated before any request is made; contradictory or unsafe input (non-Threads hosts, bad dates, empty list for the mode) fails fast with a readable message and zero charges.

### Output

Each dataset row is either a `post` or a `profile`, told apart by `record_type`. Every row also carries provenance: `source_mode`, `source_input`, `matched_keywords`, `source_tab`, `fetched_at`, `schema_version`.

#### Sample post record

```json
{
    "record_type": "post",
    "post_id": "3141592653589793238",
    "post_code": "CuXFPIeLLod",
    "post_url": "https://www.threads.com/@zuck/post/CuXFPIeLLod",
    "text": "Threads now has more than 100 million monthly actives.",
    "created_at": "2026-10-20T16:04:11.000Z",
    "author": {
        "id": "314216",
        "username": "zuck",
        "full_name": "Mark Zuckerberg",
        "is_verified": true,
        "profile_pic_url": "https://scontent.cdninstagram.com/.../avatar.jpg"
    },
    "like_count": 48210,
    "reply_count": 3120,
    "repost_count": 1875,
    "quote_count": 640,
    "view_count": null,
    "view_count_status": "unavailable",
    "media": [
        { "type": "image", "url": "https://scontent.cdninstagram.com/.../image.jpg", "width": 1080, "height": 1350, "thumbnail_url": null }
    ],
    "links": ["https://about.fb.com/news/"],
    "is_reply": false,
    "parent_post_id": null,
    "root_post_id": null,
    "quoted_post_id": null,
    "reposted_post_id": null,
    "reply_to_username": null,
    "language": "en",
    "like_and_view_counts_disabled": false,
    "source_mode": "user_posts",
    "source_input": "zuck",
    "matched_keywords": [],
    "source_tab": "threads",
    "fetched_at": "2026-11-01T10:00:00.000Z",
    "schema_version": "1.0"
}
```

#### Sample profile record

```json
{
    "record_type": "profile",
    "profile_id": "314216",
    "username": "zuck",
    "full_name": "Mark Zuckerberg",
    "profile_url": "https://www.threads.com/@zuck",
    "biography": "Building Threads at Meta.",
    "follower_count": 6500000,
    "is_verified": true,
    "is_private": false,
    "profile_pic_url": "https://scontent.cdninstagram.com/.../avatar.jpg",
    "bio_links": ["https://about.meta.com/"],
    "source_mode": "search_profiles",
    "source_input": "zuck",
    "matched_keywords": ["zuck"],
    "source_tab": "search",
    "fetched_at": "2026-11-01T10:00:00.000Z",
    "schema_version": "1.0"
}
```

#### Null semantics

`null` means "Threads did not expose this to a logged-out visitor" — never an inferred zero. `0` is an observed zero. `view_count` is currently always `null` and `view_count_status` says `"unavailable"`; when an author hides counts, `like_and_view_counts_disabled` is `true` and `like_count` is `null`. Full field semantics and the versioning policy are in [docs/schema.md](docs/schema.md). The machine-readable schema is [`.actor/dataset_schema.json`](.actor/dataset_schema.json).

Media and avatar URLs are signed by Meta's CDN and expire after some hours; download them promptly if you need the files.

### Run summary and statuses

Every run writes `RUN_SUMMARY` to the default key-value store (never to the dataset, never billed):

```json
{
    "schema_version": "1.0",
    "run_status": "partial",
    "mode": "user_posts",
    "started_at": "2026-11-01T10:00:00.000Z",
    "finished_at": "2026-11-01T10:00:41.000Z",
    "inputs_total": 3,
    "inputs_ok": 2,
    "inputs_failed": 1,
    "results_emitted": 148,
    "results_filtered": 12,
    "duplicates_suppressed": 0,
    "requests_made": 9,
    "budget_limit_reached": false,
    "inputs": [
        { "mode": "user_posts", "input": "zuck", "status": "ok", "requested_count": 100, "fetched_count": 110, "emitted_count": 98, "filtered_count": 12, "duplicates_suppressed": 0, "invalid_count": 0, "requests_made": 5, "error_code": null, "warning": null },
        { "mode": "user_posts", "input": "mosseri", "status": "ok", "requested_count": 100, "fetched_count": 50, "emitted_count": 50, "filtered_count": 0, "duplicates_suppressed": 0, "invalid_count": 0, "requests_made": 3, "error_code": null, "warning": null },
        { "mode": "user_posts", "input": "not_a_real_handle_123", "status": "not_found", "requested_count": 100, "fetched_count": 0, "emitted_count": 0, "filtered_count": 0, "duplicates_suppressed": 0, "invalid_count": 0, "requests_made": 1, "error_code": "not_found", "warning": null }
    ],
    "warnings": []
}
```

#### Per-input statuses

| Status | Meaning | Billed rows? |
|---|---|---|
| `ok` | Input processed; rows emitted (or all candidates filtered/duplicate — check the counts) | Yes, `emitted_count` |
| `partial` | Some pages succeeded, then a later page failed (e.g. a 429). Rows fetched so far were kept | Yes, what was emitted |
| `no_results` | Threads returned nothing for this input. A valid empty result, not an error | No |
| `not_found` | Unknown handle or deleted/unavailable post | No |
| `private` | Private account or login-walled content. Not collected by design | No |
| `rate_limited` | Threads throttled or blocked the requests. Retry later, keep Apify Proxy on | No |
| `unsupported` | Input we do not handle, e.g. a non-Threads URL or a profile link in `postUrls` | No |
| `failed` | Any other failure (network, format change `source_changed`, unexpected). Also used with `error_code: "not_started"` for inputs skipped after the global limit was reached | No |

#### Run statuses

| `run_status` | Rule |
|---|---|
| `succeeded` | Every input is `ok` or `no_results` |
| `partial` | Some inputs produced rows, others failed or stopped early |
| `failed` | Every input failed (`failed`, `not_found`, `private`, `rate_limited`, `unsupported`). The Actor run is marked failed so schedules and integrations notice |
| `budget_reached` | `maxTotalResults` or your run spending limit stopped the run; rows emitted so far are valid |

### Pricing

This Actor uses Apify pay-per-event with **one** event:

| Event | Price | Charged when |
|---|---|---|
| `result` | $0.0015 | A unique post or profile row is written to the dataset |

Not charged, ever: diagnostics (`RUN_SUMMARY`), rows dropped by `since`/`until`/`minLikes`, duplicates within a run (including a post matched by several keywords), rows suppressed by `onlyNew`, invalid records, and inputs that failed (`not_found`, `private`, `rate_limited`, `unsupported`, `failed`). A run that finds nothing costs nothing in results.

Worked examples: 3 usernames × 50 posts → 150 results → **$0.225**. Ten keywords in `search_posts` → at most 200 results → **≤ $0.30**. `includeProfiles` adds one `profile` result per username.

Set a **maximum cost per run** in the Apify run options to cap spending; the Actor stops cleanly with `run_status: "budget_reached"` and keeps everything it already delivered. Details, including the Console checks we do before each release so no synthetic event is priced, are in [docs/billing.md](docs/billing.md). Apify platform usage (compute, proxy, storage) is billed by Apify according to your plan and is separate from the result price.

### Limitations

These come from what Threads shows to a logged-out visitor. We measure them, document them and surface them as warnings; we do not work around them.

- **Search:** at most **20 posts per keyword per sort order**. There is no public cursor for more. No historical or exhaustive coverage is possible; use several specific keywords and both `top` and `recent` runs if you need breadth.
- **Profile search:** about **13 profiles per keyword**, single page.
- **Replies and reposts tabs:** one page each (typically 1 to 10 items). No pagination is exposed.
- **View counts:** never exposed → `view_count: null`, `view_count_status: "unavailable"`.
- **Hidden counts:** authors can hide like counts; those rows have `like_count: null` and are dropped by `minLikes`.
- **Private accounts, login walls, deleted content:** reported as `private` / `not_found`, never scraped.
- **Reply depth:** `post_urls` with `includeReplies` returns direct replies only, not nested threads.
- **Media:** URLs expire; files are not downloaded.
- **Source changes:** Threads changes its pages without notice. When our parsers no longer recognize a page they fail the input with `error_code: "source_changed"` instead of returning empty or fabricated rows. Please open an issue with the run ID.

Full list: [docs/limitations.md](docs/limitations.md).

### FAQ and troubleshooting

**My search returned fewer than 20 rows / zero rows.** Threads returns what it returns. `no_results` means the search page was empty for that keyword; try the other `sort`, a broader keyword or a hashtag. Check `fetched_count` vs `filtered_count` in `RUN_SUMMARY`: a strict `since` or `minLikes` may have filtered real matches.

**I asked for 100 results per keyword and got 20.** That is the public cap; see the warning on the input. Price is per delivered row, so you are not charged for the 80 that do not exist.

**Inputs are `rate_limited` (HTTP 429 / blocked).** Threads throttled the IP. Keep `proxyConfiguration` on Apify Proxy, lower the number of inputs per run or space scheduled runs out. The run keeps rows fetched before the throttle (`partial`). Nothing is charged for the failed inputs.

**`not_found` vs `private`.** `not_found`: the handle or post does not exist (Threads redirects to a login shell). `private`: the account exists but is private or the content sits behind a login wall; we do not collect it.

**A `/media` URL or a `threads.net` link.** Both are accepted and normalized to the canonical `threads.com/@user/post/<code>`; the row is the same post and is deduplicated against the plain link.

**Why is `view_count` null everywhere?** Threads does not show views to logged-out visitors. We would rather say `null` than guess.

**Why did I get a `profile` row I did not ask for?** You will not: profile rows only appear with `includeProfiles: true` (user modes) or in `search_profiles`.

**Scheduled runs keep returning the same posts.** Enable `onlyNew` with a `monitorName` (beta) so repeat runs only emit — and only bill — posts not seen before by that monitor. Read [docs/monitoring.md](docs/monitoring.md) first for the caveats.

**The run failed with no rows.** Every input failed; `RUN_SUMMARY.inputs[].error_code` says why for each one. Total failure is reported as a failed run on purpose, so you do not mistake it for an empty success.

### Compliance

- Collects **publicly visible** data only, exactly as shown to any logged-out visitor. No login, cookies, session reuse, CAPTCHA solving or any other access-control bypass — by policy and by code (the input schema has no place for credentials).
- Private accounts, login-walled and deleted content are skipped and reported.
- Rate limits are respected with bounded concurrency, timeouts and backoff; when Threads says no, we stop.
- You are responsible for using the data in line with Meta's Terms of Service and the data-protection law that applies to you (GDPR, CCPA and others). Store only what you need; this Actor keeps the fields in the schema and nothing else. See [docs/security-policy.md](docs/security-policy.md).

### Running locally and contributing

Requirements: Node 22 (`.nvmrc`), npm, optionally the [Apify CLI](https://docs.apify.com/cli).

```bash
npm ci
npm run typecheck && npm run lint && npm test   # offline; tests use sanitized fixtures and fakes
npm run build

## run against the live site with a local input
echo '{"mode":"user_posts","usernames":["zuck"],"maxResultsPerInput":5}' > storage/key_value_stores/default/INPUT.json
npm run dev        # or: apify run
```

`ACTOR_TEST_PAY_PER_EVENT=true` (see `.env.example`) makes the SDK log charges to `storage/datasets/charging-log` instead of billing. Tests never touch the network. Architecture, schema, billing and release notes live in [`docs/`](docs/); release steps in [docs/release-checklist.md](docs/release-checklist.md). PRs follow the template in `.github/` and need green CI (typecheck, lint, tests, build, audit).

# Changelog

This Actor's version history is a separate document: https://apify.com/scrapelabmax/meta-threads-scraper/changelog.md

# Actor input Schema

## `mode` (type: `string`):

What to scrape. <b>User posts</b>, <b>User replies</b> and <b>User reposts</b> read the <i>usernames</i> list; <b>Search posts</b> and <b>Search profiles</b> read <i>keywords</i>; <b>Post URLs</b> reads <i>postUrls</i>. Replies and reposts expose a single public page per user; search exposes at most 20 posts (or about 13 profiles) per keyword.

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

Public Threads accounts for the user modes. Accepts <code>zuck</code>, <code>@zuck</code> or <code>https://www.threads.com/@zuck</code>. Private accounts return a <code>private</code> status and no rows.

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

Search terms for the search modes, e.g. <code>open source</code> or <code>#threads</code>. Each keyword is one input. Public search returns at most 20 posts per keyword and sort order, and about 13 profiles; this is a Threads limit, not a setting.

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

Links for the <b>Post URLs</b> mode: <code>https://www.threads.com/@user/post/CODE</code>. Legacy <code>threads.net</code> links and <code>/media</code> deep links are normalized to the same post. Other hosts are rejected.

## `sort` (type: `string`):

Ordering of <b>Search posts</b> results, mirroring the Top / Recent tabs on Threads. Ignored by the other modes.

## `maxResultsPerInput` (type: `integer`):

Upper bound of post (or profile) rows emitted per username, keyword or URL. Pagination stops as soon as this many rows were accepted. The optional profile row (see <i>Include profile rows</i>) does not count against it. Search cannot exceed the 20-per-keyword source cap regardless of this value.

## `maxTotalResults` (type: `integer`):

Global cap for the whole run across all inputs. When reached, remaining inputs are not started and the run finishes with status <code>budget_reached</code>. Together with the Apify per-run spending limit this bounds your bill.

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

Keep only posts created on or after this ISO-8601 date, e.g. <code>2026-01-01</code> or <code>2026-01-01T00:00:00Z</code>. Filtering happens after fetching and before billing, so it reduces what you pay, not the number of requests. Posts without a timestamp are dropped when a date filter is set.

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

Keep only posts created on or before this ISO-8601 date (inclusive, UTC). Same format as <i>Since</i>.

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

Keep only posts with at least this many likes. Posts whose like count is hidden by the author (<code>like_count: null</code>) are dropped when this filter is set, because the value cannot be verified.

## `includeProfiles` (type: `boolean`):

User modes only. Also emit one <code>profile</code> record per username before its posts. <b>Each profile row is a separate billable result</b> ($0.0015), so it is off by default.

## `includeReplies` (type: `boolean`):

Post URLs mode only. Also emit the direct replies to each post (<code>source_tab: post_replies</code>), paginated under the same limits. Replies are billed like any other post row. Nested reply trees are not followed.

## `maxRequestsPerInput` (type: `integer`):

Hard stop on page fetches per username / keyword / URL, including the first page. Protects against runaway pagination on very large accounts. User posts return roughly 10 to 22 posts per request.

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

Suppress posts already delivered by earlier runs of the same named monitor, so a scheduled run only bills what is new. Requires <i>Monitor name</i>. Supported for User posts and Search posts. The first run of a monitor emits everything it finds. State is stored in a key-value store owned by your Apify account.

## `monitorName` (type: `string`):

Identifier for the monitor's state, e.g. <code>brand-mentions</code>. Use the same name on every scheduled run you want deduplicated against each other. Letters, digits, dashes and underscores; 1 to 63 characters.

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

Proxy settings passed to Apify Proxy. Keep Apify Proxy enabled; Threads throttles repeated requests from a single address, which shows up as <code>rate_limited</code> inputs.

## Actor input object example

```json
{
  "mode": "user_posts",
  "usernames": [
    "zuck"
  ],
  "sort": "top",
  "maxResultsPerInput": 50,
  "maxTotalResults": 1000,
  "includeProfiles": false,
  "includeReplies": false,
  "maxRequestsPerInput": 30,
  "onlyNew": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `records` (type: `string`):

All emitted post and profile rows as JSON. One billable `result` event per row. See dataset_schema.json for the fields.

## `postsView` (type: `string`):

Post rows only (profile rows hidden): URL, author, text, created, likes, replies, reposts. Use the full records link for provenance columns.

## `profilesView` (type: `string`):

Profile rows only (post rows hidden): username, name, URL, followers, verified, bio. Use the full records link for provenance columns.

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

RUN_SUMMARY record in the default key-value store: run_status (succeeded | partial | failed | budget_reached), totals, and one diagnostic per input with status, counts, error_code and warning. Never written to the dataset, never charged.

# 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"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapelabmax/meta-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 = {
    "usernames": ["zuck"],
    "proxyConfiguration": { "useApifyProxy": True },
}

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

```

## MCP server setup

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