# Tweet Replies Scraper - No Login (`renzomacar/tweet-replies-scraper`) Actor

Collect the public replies X shows to logged-out visitors on any tweet, with author, timestamp and engagement per reply. No login, no cookies, and you are never charged for a tweet that has no visible replies.

- **URL**: https://apify.com/renzomacar/tweet-replies-scraper.md
- **Developed by:** [Renzo Madueno](https://apify.com/renzomacar) (community)
- **Categories:** Social media, Automation
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.30 / 1,000 reply returneds

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?

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

## Tweet Replies Scraper - No Login

Give it a tweet. Get back the replies X shows publicly on that tweet â€” the reply text, who wrote it, when, and how much engagement each reply got â€” as clean JSON.

No login. No cookies. No browser. No Twitter account.

### Read this before you run it: coverage

X shows a logged-out visitor **the top replies only**. In every sample tested on 2026-09-04, that meant **two to three replies per tweet** â€” on a tweet with 23 replies and on a tweet with 167,872 replies alike.

The full conversation lives behind a GraphQL operation called `TweetDetail`, and that operation returns **HTTP 404 to every guest request**. Re-tested on 2026-09-04 with the query ID X's own live web app uses, alongside a control call that succeeded on the same guest token, so the 404 is not a stale identifier. It is not a matter of the right parameters or a fresher endpoint identifier: it is gated behind a signed-in session. Any cookie-free scraper claiming full reply threads is running logged-in accounts to get them.

So this actor gives you the replies X publishes, and it tells you the size of what it cannot see. Every row carries:

- `focalReplyCountReported` â€” the total reply count X itself reports for the tweet
- `repliesVisibleToGuests` â€” how many were actually retrievable

The `coverage` dataset view puts those two side by side. You are never left guessing whether you have the whole conversation.

If you need the full thread, you need a scraper that operates logged-in accounts, or the official paid X API. This README tells you that now, before you spend anything, rather than letting you find out from a dataset that looks thin.

### What those replies are actually worth

They are not random. They are the replies X's own ranking picked as most relevant, which in practice means the highest-engagement replies on the tweet. For a lot of real work that is the useful subset:

- The top reply to a viral post is usually the one that shaped how people read it.
- Community notes, corrections and the "well actually" that got 100,000 likes surface here.
- The accounts that consistently land top replies on your competitor's posts are worth knowing about.

And each reply comes back complete, not as a snippet: full text, exact timestamp, likes, retweets, quotes, bookmarks, **view count**, its own sub-reply count, any attached media, the mentions and hashtags in it, and the replier's handle, display name, numeric user ID, profile URL, avatar, verified badge and follower/following/post counts.

### Why the price still makes sense

**$0.30 per 1,000 replies, the same on every Apify plan** â€” free plan included. There is no free-tier multiplier here. At about three replies per tweet, scanning a thousand tweets costs roughly **$0.90**.

**A tweet with no visible replies costs nothing.** No row, no charge, and a `FAILURES` entry saying so. If half your input is single posts nobody replied to, you pay for the half that produced data.

### Input

```json
{
  "urls": [
    "https://x.com/jack/status/20",
    "https://x.com/elonmusk/status/1349129669258448897",
    "1519480761749016577"
  ],
  "minLikes": 100
}
```

| Field | Type | Default | What it does |
|---|---|---|---|
| `urls` | array | â€” | Tweet URLs or bare numeric IDs. Aliases: `url`, `tweetUrls`, `ids`, `tweetIds`, `startUrls`. |
| `maxItemsPerTarget` | integer | `100` | Per-tweet ceiling. Well above what X exposes, so it only bites if X widens access. |
| `maxItems` | integer | none | Ceiling across the whole run. Aliases: `maxResults`, `limit`, `maxReplies`. |
| `repliedAfter` | string | â€” | Only replies on or after this. Alias: `since`. |
| `repliedBefore` | string | â€” | Only replies on or before this. Alias: `until`. |
| `minLikes` | integer | none | Drop replies below this like count. Filtered replies are not charged. |

Full `x.com` URLs, legacy `twitter.com` URLs, `/statuses/` URLs, URLs with tracking parameters, and bare numeric IDs all work, mixed freely in one list. Duplicate tweet IDs are collapsed before anything is fetched.

Dates accept `YYYY-MM-DD`, a full ISO timestamp, or a relative window like `7 days` or `3 months`. An unreadable date raises an error naming the field and echoing what you passed, rather than being silently dropped. A window where `repliedAfter` is later than `repliedBefore` stops the run immediately instead of working through your whole list to produce nothing.

### Output

One row per reply.

| Field | Notes |
|---|---|
| `id` | The reply's own tweet ID. |
| `url` | Direct link to the reply. |
| `text` | Full reply text, including the leading `@mention`. |
| `createdAt` | ISO 8601 UTC. |
| `likeCount` | Likes on the reply. |
| `retweetCount` | Retweets of the reply. |
| `replyCount` | Sub-replies to this reply. |
| `authorHandle`, `authorName`, `authorId`, `authorUrl` | The replier. `authorId` is the stable numeric X user ID. |
| `authorProfileImageUrl` | 400x400 avatar. |
| `tweetId`, `tweetUrl`, `tweetAuthorHandle` | The tweet being replied to. |
| `focalReplyCountReported` | Total replies X reports on that tweet. |
| `repliesVisibleToGuests` | How many were actually retrievable. |
| `scrapedAt` | Fetch time. |

All fields above were present on 100% of rows in the verification run. Where a value is genuinely unavailable it is `null`, never a fabricated `0`.

Counters are normalised before they reach you: abbreviated display values become integers, and a **negative count becomes `null`** rather than being passed through, since a 32-bit overflow is not a measurement. `createdAt` is parsed from X's own timestamp format into ISO 8601, and an unreadable value becomes `null` instead of a 1970 epoch date that would slip past your date filter.

**View counts are included.** X began sending them to logged-out clients with the Sep 2026 rebuild of the signed-out page; where X omits one, the field is `null` rather than estimated.

### How it works

A logged-out `x.com` status page ships the data the page renders from, inlined in the HTML. This actor fetches the page exactly as a browser would, reads that payload, and normalises it.

Until Aug 2026 the payload was a schema.org `SocialMediaPosting` block. In Sep 2026 X rebuilt its signed-out page and the replies moved into the router's dehydrated Relay store, which is what the actor reads today - it parses that payload rather than executing it, so a page from X can never run code inside the run. Reading the data layer instead of the rendered HTML means a CSS or component change does not break the actor; a change to the data layer itself does, which is exactly what happened in Aug 2026 and what this version fixes. No headless browser is launched; the actor is a plain HTTP client at 1 GB of memory, so platform compute is negligible next to the per-result price.

Bare IDs are fetched through `x.com/i/status/<id>`, which resolves correctly without needing to know the author's handle first. That saves you a lookup and saves the run a request.

### Errors go where you can act on them

Nothing that failed is written into the dataset. It goes into the `FAILURES` key-value record:

```json
{
  "actor": "Tweet Replies Scraper",
  "targetsRequested": 3,
  "resultsDelivered": 6,
  "resultsCharged": 6,
  "failureCount": 1,
  "failures": [
    {
      "target": "1755000000000000000",
      "error": "X shows no replies publicly on this tweet, although it reports 4 in total. Nothing was charged."
    }
  ],
  "notes": [
    "On 2 tweet(s) X reports more replies than it shows to a logged-out visitor. Compare focalReplyCountReported against repliesVisibleToGuests in each row to see the gap."
  ]
}
```

There are no `{"noResults": true}` sentinel objects in your dataset. **A run that delivers nothing is marked FAILED**, not quietly succeeded over an empty dataset, and you are charged nothing either way.

### Pricing

**$0.30 per 1,000 replies. Pay per result. No subscription. Same price on every Apify plan.**

| Replies | Cost | Roughly this many tweets |
|---|---|---|
| 300 | $0.09 | 100 |
| 3,000 | $0.90 | 1,000 |
| 30,000 | $9.00 | 10,000 |

Tweets with no visible replies, deleted tweets, malformed inputs and filtered-out replies all cost $0.00.

### Typical uses

- **Reaction sampling at scale.** Run it over a few thousand posts from a brand, a category or an event and read the top reply on each. It is a fast, cheap read on how a message landed.
- **Finding the loudest voices.** The `repliers` dataset view lists the accounts writing top replies, with their user IDs. Feed those handles into the X Profile Scraper in this fleet to size them up.
- **Correction and crisis monitoring.** The reply that gets the most likes on a wrong claim is usually the correction. It is the one this actor returns.
- **Competitive listening.** Who reliably lands the top reply under your competitor's announcements, and what do they say.
- **Sentiment sampling with honest error bars.** Three top replies is a biased sample and you should treat it as one â€” but `focalReplyCountReported` tells you exactly how biased, which is more than most reply data comes with.

### Limits worth knowing before you start

- **Three replies per tweet, not the whole thread.** Covered at the top. This is X's limit for logged-out clients, verified across sample tweets ranging from 23 to 167,872 replies.
- **The visible replies are ranked by X, not chronological.** They are the top replies, not the first ones.
- **Nested sub-replies are not fetched.** Each row reports its own `replyCount`, but the branch below it is not walked â€” X does not expose it publicly either.
- **Deleted, protected and suspended-account tweets return nothing** and are reported as failures rather than billed.
- **No keyword search.** This actor takes tweets, not queries. X blocks the search timeline for guests entirely.

### Verified

Every endpoint, field and coverage figure in this README was tested live against X on **2026-09-04** with no cookies and no account, through residential IPs. The two-to-three reply ceiling and the `TweetDetail` 404 are measurements taken that day, not assumptions.

### Related actors

- **Tweet Scraper by URL or ID** â€” the tweet itself with full metrics, $0.20/1k.
- **X Profile Scraper - No Login** â€” turn repliers into full profiles, $0.25/1k.
- **X Profile Posts Scraper - No Cookies** â€” an account's timeline, $0.30/1k.
- **X Trends Scraper by Country** â€” 62 countries and 400+ cities, $0.40/1k.

Same rules across all of them: one price on every plan, and nothing charged for nothing.

# Actor input Schema

## `urls` (type: `array`):

One tweet per line. Accepts full URLs (https://x.com/jack/status/20) or bare numeric IDs (20). Note what X allows here: a logged-out client sees the top replies only, typically three per tweet. Read "Coverage" in the description before you plan a run around this.

## `maxItemsPerTarget` (type: `integer`):

Per-tweet ceiling. Defaults to 100, which is far above what X actually exposes publicly, so in practice it only matters if X widens access.

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

Ceiling across every tweet in the run. Leave empty for no overall cap.

## `repliedAfter` (type: `string`):

YYYY-MM-DD, a full ISO timestamp, or a relative window such as "7 days". Replies outside the window are dropped before billing.

## `repliedBefore` (type: `string`):

YYYY-MM-DD, a full ISO timestamp, or a relative window such as "1 day".

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

Drop replies below this like count. Filtered replies are never charged.

## Actor input object example

```json
{
  "urls": [
    "https://x.com/jack/status/20"
  ],
  "maxItemsPerTarget": 100,
  "repliedAfter": "2024-01-01",
  "repliedBefore": "2026-01-01"
}
```

# Actor output Schema

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

One row per publicly visible reply: author, text, engagement counts and timestamp.

# 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 = {
    "urls": [
        "https://x.com/jack/status/20",
        "https://x.com/elonmusk/status/1349129669258448897"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("renzomacar/tweet-replies-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "urls": [
        "https://x.com/jack/status/20",
        "https://x.com/elonmusk/status/1349129669258448897",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("renzomacar/tweet-replies-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "urls": [
    "https://x.com/jack/status/20",
    "https://x.com/elonmusk/status/1349129669258448897"
  ]
}' |
apify call renzomacar/tweet-replies-scraper --silent --output-dataset

```

## MCP server setup

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

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/HLsSJhH4JHiNnyvLB/builds/hIehUL2gCD0tkMhMK/openapi.json
