# Twitter X Reply Scraper (`scraper-engine/twitter-x-reply-scraper`) Actor

🐦 Twitter X Reply Scraper (twitter-x-reply-scraper) extracts public replies from X posts—usernames, handles, timestamps, text, and engagement—at scale. 🔎 Threads, filters & pagination. 📄 Export CSV/JSON. ⚡ Perfect for social listening, brand monitoring, research & customer support.

- **URL**: https://apify.com/scraper-engine/twitter-x-reply-scraper.md
- **Developed by:** [Scraper Engine](https://apify.com/scraper-engine) (community)
- **Categories:** Social media, Developer tools, Automation
- **Stats:** 22 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.99 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

### Twitter (X) Reply Scraper — Reply Text, Authors and Likes

Twitter (X) Reply Scraper collects public posts from any x.com search URL together with the direct replies underneath each one, and returns them as structured JSON: reply text in `tweetContent`, the replier's `handle`, `fullname` and `avatar`, an ISO-8601 `tweetDate`, and `likeCount`, `retweetCount`, `quoteCount` and `commentCount` on the post and on every reply. Rows stream into your dataset as each post finishes — export to JSON, CSV or Excel. Paste a search URL, add your X session, press Start.

⚠️ **This Actor cannot run without your own X session cookies.** `authToken` and `ct0` are not flagged `required` in the schema, but the run validates both before it does anything else: if either is empty, it logs `Add your session in the input (auth token + ct0) or set AUTH_TOKEN and CT0.` and exits immediately — no posts, no replies, no dataset rows, nothing charged. Read the session section below before your first run.

### What is Twitter (X) Reply Scraper?

Twitter (X) Reply Scraper is an Apify Actor that runs an X search, then opens each post it finds and extracts the replies written directly to that post. Each dataset row is one post with its replies nested inside a `repliesData` array — twelve typed top-level keys, no HTML parsing on your side.

An X login **is** required. Not an Apify-side account, but your own browser session, supplied as the `auth_token` and `ct0` cookies. X answers search and conversation requests only for an authenticated session, which is a platform behaviour rather than an Actor limitation.

It is built for PR and media teams working reply threads, social listening and community analysts, lead researchers mining conversations, and developers or AI engineers piping reply threads into their own pipelines.

### What X reply data is publicly available to scrape?

X exposes a reply exactly as any signed-in visitor sees it in the conversation view: the reply text, who wrote it, when, and its own engagement counts. What it does not expose is anything from a protected account — and anything below the first level of the thread is not something this Actor collects.

| Data Category | Returned by this Actor | Not returned |
| ----- | ----- | ----- |
| Reply text, reply permalink, reply timestamp in UTC | ✅ | — |
| Replier handle, display name, avatar image URL, verified flag | ✅ | — |
| Reply engagement — likes, reposts, quotes, reply count | ✅ | — |
| The parent post — text, author, permalink, engagement counts | ✅ | — |
| Replies to replies — second and deeper levels of the thread | ❌ | Only replies whose parent is the post itself are kept |
| Media on a post or reply — photos, videos, cards, hashtags, language | ❌ | Present in X's payload, not extracted by this Actor |
| View counts and bookmark counts | ❌ | Not extracted by this Actor |
| Replies from protected (private) accounts | ❌ | X withholds them from search and conversation views |
| Replier email, phone or follower list | ❌ | Never in a reply payload |

Twitter (X) Reply Scraper only returns publicly visible data — what any signed-in visitor sees on the search page and in the conversation. Nothing from a protected account, and nothing from your own private timeline.

### ⚠️ You must supply your own X session cookies

This is the one hard prerequisite, and it is worth stating plainly instead of burying it in the input table.

X serves its `SearchTimeline` and `TweetDetail` GraphQL endpoints only to an authenticated client. This Actor sends your two cookies with every request, and additionally sends `ct0` back as the `x-csrf-token` header, which is what X expects from its own web app. There is no guest path and no fallback: the run checks both values first and stops if either is missing.

Supply two cookie values from a browser where you are already signed in to x.com:

- `auth_token` — your X session token, pasted into `authToken`
- `ct0` — the CSRF token that pairs with it, pasted into `ct0`

Get both from DevTools → **Application** → **Cookies** → `https://x.com`, or from a cookie-export extension. Alternatively set them as the `AUTH_TOKEN` and `CT0` environment variables on the Actor; the input fields take precedence over the environment.

Both fields are declared `isSecret` in the input schema, so Apify stores them encrypted and masks them in the Console and in the run's input record. The Actor never writes either value to the dataset or to the log.

Two things to weigh before you paste a session in. Replaying a live browser session from a datacenter or proxy IP is exactly the pattern X's automated-behaviour rules are written against, so the account whose cookies you use carries a real risk of being rate-limited, challenged or locked — prefer a secondary account you can afford to lose, and refresh the cookie if it stops working. And every request the Actor makes counts against that account's own quota, which is why a run can start returning nothing part-way through even though the cookies were valid an hour earlier.

### What data can I extract with Twitter (X) Reply Scraper?

Every row carries the parent post, its author's public identity, its engagement counts, and the replies nested inside it. Twelve top-level keys, written on every row of every run.

| Field Name | Description |
| ----- | ----- |
| `tweetLink` | Permalink to the post, built as `https://x.com/<handle>/status/<id>` |
| `tweetContent` | Full post text, from X's `full_text` — long-form posts arrive complete, not truncated |
| `tweetDate` | Post time as an ISO-8601 UTC string, e.g. `2026-07-24T13:38:02.000Z`. Milliseconds are always `.000`, because X publishes second precision |
| `handle` | Author's handle including the `@`, e.g. `@meehikabarua` |
| `fullname` | Author's display name as X renders it |
| `avatar` | Author's profile image URL, rewritten from X's `_normal` size to the larger `_bigger` variant |
| `verified` | `true` when the author carries a blue check or a verification record. Otherwise `null` — this key is never `false` |
| `commentCount` | X's own reply count for the post — the true total, usually larger than the number of entries in `repliesData` |
| `retweetCount` | Reposts of the post |
| `quoteCount` | Quote posts of the post |
| `likeCount` | Likes on the post |
| `repliesData` | Array of direct replies to this post, each with eleven keys of its own — see below. Empty array when there are none |

#### Post identity and author fields

`tweetLink`, `tweetContent`, `tweetDate`, `handle`, `fullname`, `avatar` and `verified` describe the post and who wrote it. Missing values arrive as empty strings rather than `null`: an author with no resolvable screen name yields `handle` of `"@"`, an account with no avatar yields `avatar` of `""`, and a post whose timestamp X did not ship yields `tweetDate` of `""`. If X ever returns a date in a format the parser does not recognise, the raw X string is passed through unchanged instead of being dropped.

#### Engagement counts

`commentCount`, `retweetCount`, `quoteCount` and `likeCount` are integers taken straight from X's counters, and default to `0` when absent. `commentCount` is the count X displays on the post, not the length of `repliesData` — comparing the two tells you what share of a thread you actually read. There is no view count and no bookmark count in this output.

#### The `repliesData` array

| Field Name | Description |
| ----- | ----- |
| `repliesData[].tweetLink` | Permalink to the individual reply |
| `repliesData[].tweetContent` | The reply text |
| `repliesData[].tweetDate` | Reply time as an ISO-8601 UTC string |
| `repliesData[].handle` | Replier's handle including the `@` |
| `repliesData[].fullname` | Replier's display name |
| `repliesData[].avatar` | Replier's profile image URL, `_bigger` variant |
| `repliesData[].verified` | `true` or `null`, same rule as on the post |
| `repliesData[].commentCount` | Replies to that reply — a count only; those reply objects themselves are not collected |
| `repliesData[].retweetCount` | Reposts of that reply |
| `repliesData[].quoteCount` | Quote posts of that reply |
| `repliesData[].likeCount` | Likes on that reply |

A reply object repeats the post shape minus `repliesData` itself, so the nesting is exactly one level deep. Only replies whose parent is the post are kept: the Actor reads X's conversation payload and keeps an entry only when its `in_reply_to_status_id_str` equals the post's own ID, which means a sub-thread hanging off another reply is filtered out even though it arrived in the same response.

One naming note, stated plainly so your field mapping matches what arrives: every key is camelCase except `fullname`, which is all lowercase — not `fullName`. It is spelled the same way on the post and inside `repliesData`.

#### 🤖 Add-on: Need additional X data?

**Twitter / X Posts Scraper** covers a specific account's timeline rather than a search, and it needs no cookies at all — the right tool when you want an account's own posts and no reply threads. **X (Twitter) Posts Search** is the deeper search Actor: it takes profiles, handles, keywords, hashtags or search URLs, offers a Top-or-Latest mode and day/hour time windows, and returns media, language and author stats — but no reply threads. **Twitter Trends Scraper** gives you the trend boards for 62 countries plus worldwide with no login required, which is a good way to pick the search terms you then feed into this Actor.

### Why not build this yourself?

X does publish an official API. Access to it is a paid, tiered product and reply/conversation access depends on the tier, so for most projects the real question is not "API or scraper" but what the scraper has to absorb. This is what this one absorbs.

- **Session plumbing that X validates in three places.** The two cookies go on the request as cookies, `ct0` goes again as the `x-csrf-token` header, and the request must also carry X's web bearer token and its `OAuth2Session` auth-type header. Get one of the four wrong and X answers with an error payload that still has HTTP 200 on it.
- **GraphQL query IDs that rotate on every X deploy.** `SearchTimeline` and `TweetDetail` live at URLs containing an opaque per-operation ID. The Actor discovers both at runtime — it reads x.com to find the current `main.<hash>.js` bundle, downloads that bundle from X's CDN, and regex-scans it for the operation-to-query-ID mapping, caching the result for five minutes and falling back to a known-good pair if discovery fails. Hardcode those IDs and your scraper breaks on X's next front-end release.
- **The `x-client-transaction-id` header.** X's web app derives a per-request transaction ID from its home page and its `ondemand.js` file. The Actor builds a generator from both, caches it for an hour, and rebuilds it whenever the exit IP changes.
- **Conversation-thread traversal.** Replies do not arrive as a flat list. They come as `conversationthread-` timeline entries whose items may be plain tweets or `TweetWithVisibilityResults` wrappers, each needing to be unwrapped before its author can be resolved through a nested user object — and then filtered on parent ID so quoted posts and sub-threads do not contaminate the reply set.
- **Search pagination.** X returns 20 results per search call, and the cursor to the next page has to be dug out of the timeline instructions, where it can appear as a `Bottom` cursor, a `showMoreThreads` cursor, or inside a replace-entry instruction rather than an add-entries one.
- **The proxy ladder.** HTTP 401, 403, 429 and 503 all mean "change something", and so does a 200 carrying a GraphQL `errors` array. Escalating direct → datacenter → residential in the right order, and pinning the rung that worked so the rest of the run reuses it, is the difference between a cheap run and an expensive one.

### How to use Twitter (X) Reply Scraper

Twitter (X) Reply Scraper runs on Apify. Start it from the Apify Console or call it through the Apify API — your Apify token is the only Apify-side credential involved, alongside the X cookies described above.

1. Open Twitter (X) Reply Scraper on Apify and click **Try for free**
2. Paste one or more x.com **search** URLs into **Start URLs** (`startUrls`). The form arrives with `https://x.com/search?q=%23journorequest+min_replies%3A10&src=typed_query&f=live` pre-filled as a working example
3. Set **Maximum posts to collect** (`maxSearchResults`) and **Maximum replies per post** (`maxReplies`)
4. Paste your `auth_token` into **Session — auth token** (`authToken`) and your `ct0` into **Session — security (ct0)**. The run stops without both
5. Leave the proxy off to begin with — the Actor escalates on its own when X pushes back
6. Click **Start**, then export as JSON, CSV or Excel, or read the dataset through the Apify API

Build the search URL on x.com itself, or with [X advanced search](https://x.com/search-advanced), and paste the result. Anything you can express in an X search query works, because the query string is passed through to X untouched — `min_replies:10` to find posts that already have a conversation, `from:` and `to:` to scope by account, `-word` to exclude, `lang:` to restrict language, a date range, or a hashtag.

Only the `q` parameter of the URL is read. The rest — `f=live`, `f=top`, `src=typed_query` — is discarded, and the Actor always requests X's **Latest** product, so a Top-tab URL and a Latest-tab URL give the same ordering. A URL with no `q` at all, which includes any single-post `.../status/123...` link, silently falls back to the Actor's built-in default query `#journorequest min_replies:10` — so pasting a post URL does not scrape that post, it scrapes the default search. Paste searches, not posts.

#### How to scrape replies from multiple X searches in one run

`startUrls` is a list, so bulk collection is the default mode. Searches are processed strictly one at a time, and `maxSearchResults` applies **per search URL, not per run** — three searches at 50 posts each is a 150-row run. `maxReplies` then applies per post inside that. Each entry may be a plain string or an object with a `url` key, and the two forms can be mixed in one list.

Between pages of a search the Actor waits 0.3 seconds, and between posts 0.5 seconds plus up to 0.2 seconds of jitter. That pacing is deliberate — it is what keeps your session's quota from being burned in a burst. There is no deduplication anywhere in the run: if two of your searches match the same post, or X returns an overlapping entry across cursor pages, that post is written to the dataset twice. Deduplicate on `tweetLink` downstream if it matters.

### What can you do with X reply data?

- 📣 **A PR or media-relations specialist** monitoring `#journorequest` searches on `min_replies:10` reads `tweetContent` for the request itself, then works `repliesData[].handle` and `repliesData[].tweetDate` to see who already answered and how quickly, instead of pitching into a thread that closed yesterday.
- 🎯 **A lead researcher** mining a competitor's announcement post builds a list from `repliesData[].handle`, `repliesData[].fullname` and `repliesData[].verified`, sorted on `repliesData[].likeCount` so the replies the audience actually agreed with come first.
- 📊 **A social listening analyst** tracking reaction to a launch pulls every reply's `tweetContent` and `tweetDate` across a week of searches, and checks `commentCount` against the length of `repliesData` on each row to know whether a thread was read in full or sampled before drawing a conclusion from it.
- 🔍 **A community manager** finding the voices worth engaging ranks `repliesData` entries by `likeCount` and `retweetCount` and filters on `verified`, turning a 400-reply thread into the ten accounts whose reply travelled.
- 🤖 **An AI engineer** building a social-listening agent indexes the post's `tweetContent` together with each `repliesData[].tweetContent` as one document per thread, so the agent answers "how did people respond to this" from the actual conversation rather than a summary, with `tweetLink` as the citation anchor.

Every one of these is callable from an agent framework over the Apify API, since the Actor is a standard HTTP-triggered run returning typed JSON.

### How does Twitter (X) Reply Scraper handle rate limits and blocking?

Requests are plain HTTP with X's own web-app headers — no headless browser, and no CAPTCHA solving of any kind. What the Actor does handle for you is escalation.

**A proxy ladder that ratchets upward.** A run starts on a direct connection, or on Apify Proxy if you enabled it. When X pushes back the Actor climbs to an Apify datacenter group, then to Apify residential, retrying up to three times there with a growing backoff. The rung that finally worked is pinned and reused as the first attempt for your remaining search URLs, so one bad start does not make the whole run expensive. Every escalation also clears the cached GraphQL query IDs and the transaction-ID generator and rebuilds them on the new IP, because both are IP-bound.

**Blocking detected on more than the status code.** HTTP 401, 403, 429 and 503 are all treated as "escalate", and so is a transport failure with no status at all. Crucially, a 200 response carrying a GraphQL `errors` array is treated the same way — X frequently reports a rate limit or an invalid session inside a successful response, and trusting the status code alone would record an empty search as a real result.

Two honest gaps. That escalation logic guards the **search** stage only: if the reply request for an individual post fails or is rate-limited, the post is still written with `repliesData` as `[]` and still charged, and no field distinguishes "this post has no replies" from "the reply request failed". And when every rung of the ladder fails for a search URL, the Actor logs `Could not finish after several tries. Check your session or try again later.` and moves on to your next URL — it writes no error row, so a search that produced nothing leaves no trace in the dataset. Read the log alongside the dataset when a run comes back short.

### ⬇️ Input

Read straight from the Actor's input schema. `startUrls` is the only parameter marked required — but see the note under the table, because two more are required in practice.

| Parameter | Required | Type | Description | Example Value |
| ----- | ----- | ----- | ----- | ----- |
| `startUrls` | Yes | array | One or more x.com search URLs. Only the `q` query parameter is read. Each entry may be a plain string or an object with a `url` key. Pre-filled with one working example search. | `["https://x.com/search?q=%23journorequest+min_replies%3A10&src=typed_query&f=live"]` |
| `maxSearchResults` | No | integer | How many posts to collect from each search URL. Minimum 1, maximum 5000, default `10`. Applies per search URL, not per run. | `30` |
| `maxReplies` | No | integer | How many replies to keep for each post. Minimum 1, maximum 500, default `5`. Applies per post. | `10` |
| `authToken` | No | string | Your X `auth_token` cookie value. Stored encrypted (`isSecret`). Falls back to the `AUTH_TOKEN` environment variable. The run stops without it. | `"a1b2c3d4e5f60718293a4b5c6d7e8f9012345678"` |
| `ct0` | No | string | Your X `ct0` cookie value, also sent as the `x-csrf-token` header. Stored encrypted (`isSecret`). Falls back to the `CT0` environment variable. The run stops without it. | `"9f8e7d6c5b4a39281706f5e4d3c2b1a0"` |
| `proxyConfiguration` | No | object | Apify Proxy settings. Pre-filled as off — the Actor escalates from direct to datacenter to residential on its own. Enabling it simply starts the ladder one rung up. | `{"useApifyProxy": false}` |

**`authToken` and `ct0` are required in practice.** Neither is in the schema's `required` list, but the run validates both before touching X and exits immediately if either is empty. There is no partial mode: one cookie without the other does nothing.

Four further behaviours worth knowing, all from the Actor's own input handling:

- **`maxReplies` cannot fetch more than one conversation page.** The Actor makes exactly one reply request per post and does not paginate the conversation, so `maxReplies` trims what that single response contained rather than reaching for more. Setting it to 500 is accepted and will not error, but on a heavily-replied post you get what X put in one page of direct replies, not 500. It also cannot be set to `0` — the schema minimum is 1, so every post row comes with a reply lookup attached.
- **Both limits are coerced, not rejected.** A missing, empty or zero `maxSearchResults` falls back to `10`; `maxReplies` falls back to `5`. Values above the maximum are clamped to 5000 and 500 respectively, and values below 1 are raised to 1 — matching the schema bounds exactly.
- **`maxSearchResults` is a ceiling, not a promise.** X returns 20 posts per search call and the Actor paginates until it has enough, but it stops early and keeps what it has whenever X returns no next cursor or an empty page.
- **Two undocumented input aliases exist.** `urls` is accepted in place of `startUrls`, and `auth_token` in place of `authToken`. They work, but the schema names are the ones the Console form writes.

#### Example input

```json
{
  "startUrls": [
    "https://x.com/search?q=%23journorequest+min_replies%3A10&src=typed_query&f=live",
    { "url": "https://x.com/search?q=%22launching+today%22+min_faves%3A50&src=typed_query&f=live" }
  ],
  "maxSearchResults": 30,
  "maxReplies": 10,
  "authToken": "a1b2c3d4e5f60718293a4b5c6d7e8f9012345678",
  "ct0": "9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a39281706f5e4d3c2b1a0",
  "proxyConfiguration": { "useApifyProxy": false }
}
```

### ⬆️ Output

One row per post, twelve top-level keys, the same shape on every run. Rows are pushed the moment each post's replies are collected, so results appear in the Output tab while the run is still going. Export as JSON, CSV or Excel, or read the dataset through the Apify API.

The default dataset table view shows eight columns — `tweetLink`, `tweetContent`, `tweetDate`, `handle`, `commentCount`, `retweetCount`, `likeCount` and `repliesData`. The other four keys are in every record regardless: `avatar`, `fullname`, `verified` and `quoteCount`. Use the JSON export or the API when you want them as first-class columns.

Two things about the row shape. Missing values arrive as empty strings or `0` rather than `null`, with one exception: `verified` is `true` or `null` and never `false`. And there are **no error rows and no accounting rows in this dataset at all** — no `isError`, no `errorReason`, no `status` key, nothing to filter on. Every record is a real post row, and every record fires one `row_result` charge, including a post whose `repliesData` came back empty. Charging is per post, not per reply, so a run of 30 posts costs 30 events whether each carries one reply or fifty. Failures are reported in the run log only.

#### Example output

```json
{
  "tweetLink": "https://x.com/meehikabarua/status/1948214563127839204",
  "avatar": "https://pbs.twimg.com/profile_images/1690785189792735232/BmUFicth_bigger.jpg",
  "fullname": "Meehika Barua",
  "handle": "@meehikabarua",
  "verified": null,
  "tweetDate": "2026-07-24T13:38:02.000Z",
  "tweetContent": "Looking for menswear experts to comment on what to wear to a job interview for a Men's Journal piece. Deadline 28th July. Email in bio.\n#journorequest #prrequest",
  "commentCount": 41,
  "retweetCount": 4,
  "quoteCount": 1,
  "likeCount": 27,
  "repliesData": [
    {
      "tweetLink": "https://x.com/editorielle/status/1948217841203994112",
      "avatar": "https://pbs.twimg.com/profile_images/1730590378942681088/MbS2v-ce_bigger.jpg",
      "fullname": "EDITORIELLE",
      "handle": "@editorielle",
      "verified": true,
      "tweetDate": "2026-07-24T13:51:09.000Z",
      "tweetContent": "We've shared this with our fashion network for you, Meehika. Thanks, Bethany x",
      "commentCount": 1,
      "retweetCount": 0,
      "quoteCount": 0,
      "likeCount": 6
    },
    {
      "tweetLink": "https://x.com/MediaMatchMaker/status/1948231004556218369",
      "avatar": "https://pbs.twimg.com/profile_images/1148575226432802818/nLiLszhz_bigger.png",
      "fullname": "MediaMatchMaker",
      "handle": "@MediaMatchMaker",
      "verified": true,
      "tweetDate": "2026-07-24T14:43:35.000Z",
      "tweetContent": "Hi Meehika, we've passed this to two stylists who cover interview dressing. Sending intros by email now.",
      "commentCount": 0,
      "retweetCount": 1,
      "quoteCount": 0,
      "likeCount": 3
    },
    {
      "tweetLink": "https://x.com/danielokaforpr/status/1948244118862512640",
      "avatar": "https://pbs.twimg.com/profile_images/1854884381308895232/uPeanSn1_bigger.jpg",
      "fullname": "Daniel Okafor",
      "handle": "@danielokaforpr",
      "verified": null,
      "tweetDate": "2026-07-24T15:35:47.000Z",
      "tweetContent": "Dropped you an email - I work with two tailors in Lagos who would be great on this.",
      "commentCount": 0,
      "retweetCount": 0,
      "quoteCount": 0,
      "likeCount": 2
    }
  ]
}
```

And a post that matched the search but had no direct replies the Actor could read — the row is still written, and still charged:

```json
{
  "tweetLink": "https://x.com/kintijapr/status/1948190447336419840",
  "avatar": "https://pbs.twimg.com/profile_images/1767833219280580608/MR3fO5Yd_bigger.jpg",
  "fullname": "kintija / kiki",
  "handle": "@kintijapr",
  "verified": null,
  "tweetDate": "2026-07-24T12:02:15.000Z",
  "tweetContent": "#journorequest - after UK-based sleep researchers for a short explainer. Deadline Friday.",
  "commentCount": 12,
  "retweetCount": 2,
  "quoteCount": 0,
  "likeCount": 9,
  "repliesData": []
}
```

Note the `commentCount` of 12 against an empty `repliesData` in that second record. That gap is the honest signal to watch: it means X's counter includes replies this Actor did not return — because they sat below the first level of the thread, came from protected accounts, or the reply request itself did not succeed.

### How does it work?

Each search URL you supply is reduced to its `q` query string and sent to X's own `SearchTimeline` GraphQL endpoint with your session cookies attached, twenty posts at a time, following X's cursor until the Actor has as many posts as you asked for. Every post is then looked up once against the `TweetDetail` endpoint, which returns the conversation around it; the Actor unwraps each conversation entry, resolves the replier from the nested user object, keeps only entries whose parent is the post itself, trims the list to `maxReplies`, and writes the finished row to your dataset straight away.

The endpoint IDs and the per-request transaction header are not hardcoded values — they are discovered at runtime from X's own front-end bundle and cached, then rebuilt whenever the exit IP changes, which is what keeps the Actor working across X's deploys. Requests begin direct and escalate through Apify datacenter and residential proxies only when X pushes back. Only publicly visible posts and replies are returned, and the twelve-key output shape stays the same regardless of how X changes its interface.

### Integrations

Twitter (X) Reply Scraper is an Apify Actor, so it works with anything that can call the Apify API or consume a dataset.

#### Calling Twitter (X) Reply Scraper from Python

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")

run = client.actor("<YOUR_USERNAME>/twitter-x-reply-scraper").call(run_input={
    "startUrls": ["https://x.com/search?q=%23journorequest+min_replies%3A10&src=typed_query&f=live"],
    "maxSearchResults": 30,
    "maxReplies": 10,
    "authToken": "<YOUR_X_AUTH_TOKEN_COOKIE>",
    "ct0": "<YOUR_X_CT0_COOKIE>",
})

for post in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(post["tweetDate"], post["handle"], post["likeCount"], len(post["repliesData"]))
    for reply in post["repliesData"]:
        print("   ", reply["handle"], reply["likeCount"], reply["tweetContent"][:80])
```

Works in Go, Ruby, Node.js, cURL — any language that can make an HTTP request. Because the dataset carries no error rows, no filtering step is needed before you iterate.

#### No-code tools (n8n, Make, LangChain)

In n8n, use the Apify node — or an HTTP Request node pointed at the Actor's run endpoint with your token — and pass the same JSON input shown above; an Item Lists node then splits `repliesData` into one item per reply for downstream steps. In Make, the Apify module supports run-and-wait, so a scheduled sweep of a saved X search can feed a Google Sheets, Airtable or CRM step directly. In LangChain, wrap the run call as a tool function and hand the rows to your agent as context — they are already typed JSON, so no output parser is needed.

### Is it legal to scrape X replies?

Scraping publicly accessible pages is broadly treated as permissible where no authentication is bypassed, and this Actor returns only public posts and public replies, read with credentials you supply for an account you control. But the output is unambiguously **personal data**, and it is worth treating it that way from the first run.

`handle`, `fullname` and `avatar` identify a named individual, `tweetContent` is that individual's own written expression, and `tweetLink` and `tweetDate` tie the two together — and all five appear again for every replier inside `repliesData`. Under GDPR, UK GDPR and the CCPA, collecting and storing this is processing, and the fact that data is publicly available is not itself a lawful basis.

In practice that means four things. Pick and document a lawful basis before you store rows — for research and monitoring that is usually legitimate interest, which requires a written balancing test weighing your purpose against the replier's reasonable expectation that a public reply would be aggregated into someone's database. Minimise: drop `avatar` first, since a face photo adds nothing to most analyses, then `fullname`, and if you only need engagement volume keep the counts and discard `tweetContent` and `handle` too. Set a retention period and delete on schedule rather than accumulating indefinitely. And stay able to honour access, objection and erasure requests, which means keeping `tweetLink` so one person's records can be located and removed; if you are a controller in scope of the GDPR's transparency rules, Article 14 also obliges you to inform people whose data you collected without contacting them first.

Using this data for outreach is a separate regime from collecting it. Marketing and cold-contact rules — PECR and its equivalents in the EU, CAN-SPAM in the US, plus X's own terms if you message or reply on-platform — apply to the outreach itself, and a valid basis for collection does not carry over to it. Consult legal counsel if your use case involves bulk storage of personal data, AI training corpora, or resale.

### ❓ Frequently asked questions

#### What X reply fields does Twitter (X) Reply Scraper return?

The five most used are `repliesData[].tweetContent`, `repliesData[].handle`, `repliesData[].tweetDate`, `repliesData[].likeCount` and the parent post's own `tweetContent`. Each row also carries `tweetLink`, `fullname`, `avatar`, `verified`, `commentCount`, `retweetCount` and `quoteCount` on the post, and the same eleven-key shape on every reply. Twelve top-level keys in total — see the data fields table above.

#### Does Twitter (X) Reply Scraper require an X account or login?

Yes, and this one is mandatory. X answers its search and conversation endpoints only for an authenticated session, so you must supply your own `auth_token` and `ct0` cookies in `authToken` and `ct0`, or as the `AUTH_TOKEN` and `CT0` environment variables. Neither field is marked required in the schema, but the run validates both first and exits immediately if either is empty — no rows, no charge. Both are stored encrypted as secret inputs and are never written to the dataset or the log. Prefer a secondary X account: replaying a live session through automation risks that account being rate-limited or locked.

#### How many posts and replies can I get in one run?

`maxSearchResults` accepts 1 to 5000 and defaults to 10, applied **per search URL**, so run size is your number of searches multiplied by that limit. `maxReplies` accepts 1 to 500 and defaults to 5, applied per post. The real reply ceiling is X's rather than the Actor's: one conversation request is made per post and the conversation is not paginated, so `maxReplies` trims that single response instead of fetching more. On a lightly-replied post you get the whole thread; on a viral one you get the direct replies X put in one page. `commentCount` on each row tells you which case you are in.

#### What happens if a post has no replies, is deleted, or the account is protected?

You get a row with `repliesData` as `[]`, and it is charged like any other row. A post with no replies, a post whose replies all sit deeper in the thread, a deleted post, a protected account whose replies X will not serve, and a reply request that was rate-limited all produce the same empty array — there is no `isError`, `errorReason` or `status` key to tell them apart, because this Actor writes no error rows. The one signal available is the gap between `commentCount` and the length of `repliesData`. A post deleted between the search and the reply lookup simply comes back with no replies rather than failing the run. When a whole search URL cannot be read after the proxy ladder is exhausted, the Actor logs the failure and moves to your next URL without writing anything for it, so check the log if a run returns fewer rows than you expected.

#### Can I paste a single post URL instead of a search URL?

No, and this is the most common surprise. The Actor reads only the `q` query parameter out of each start URL. A `https://x.com/user/status/123...` link has no `q`, so it falls back to the Actor's built-in default query `#journorequest min_replies:10` — the run succeeds and returns rows, but for that default search rather than the post you pasted. Use an x.com search URL, and if you want the replies to one specific post, target it with search operators such as `to:username` plus a date range.

#### Can I scrape replies from multiple X searches at once?

Yes — `startUrls` is a list and bulk is the normal mode. Entries may be plain strings or objects with a `url` key, mixed freely. Searches run one at a time, with 0.3 seconds between search pages and 0.5 seconds plus jitter between posts, which is deliberate pacing to protect your session's quota. Be aware there is no deduplication: a post matched by two of your searches, or returned twice across cursor pages, is written twice. Deduplicate on `tweetLink` downstream.

#### Does Twitter (X) Reply Scraper work with Claude, ChatGPT and other AI agent tools?

Yes. It is callable as a standard HTTP endpoint through the Apify API, so LangChain, CrewAI, n8n or a hand-written tool definition can invoke it and receive typed JSON with no parsing step. There is no separate signup and no separate API key — your Apify token plus the X cookies are the only credentials.

#### How does Twitter (X) Reply Scraper compare to other X reply scrapers?

Checked on the Apify Store on 25 July 2026. `louisdeconinck/twitter-reply-scraper` documents the same input parameters (`startUrls`, `maxSearchResults`, `maxReplies`) and the same output field names used here, and its listing explicitly advertises "No Authentication Required: No need for cookies or authentication, removing the risk of bans" — a genuine advantage over this Actor on that axis, and worth weighing if you would rather not expose a session. Its listing also documents setting `maxReplies` to `0` to skip replies entirely, which this Actor's schema does not allow (minimum 1). `kaitoeasyapi/twitter-reply` takes a tweet ID rather than a search URL, so it fetches replies for posts you already have instead of finding the posts for you; its listing states no Twitter authentication is needed and advertises $0.25 per 1,000 tweets, 150 tweets per second, 99.9% uptime and no rate limits — all figures published by that Actor, none of them measured here. `fastcrawler/twitter-reply-scraper-0-2-1k-tweets-pay-per-result-2025` is listed under a pay-per-result title advertising $0.2 per 1,000 tweets, but its README returned HTTP 404 when checked, so nothing about its inputs or output fields is documented.

What this Actor documents that those listings do not: the exact two-stage flow — search first, then one conversation lookup per post — with `maxSearchResults` stated as per-URL; the precise reply filter, direct replies only, keyed on the parent post ID; the runtime discovery of X's rotating GraphQL query IDs; and plain disclosure that there are no error rows, that charging is per post rather than per reply, and that no deduplication happens. Where a competitor's listing is silent on one of these, that is not documented rather than not supported.

#### Does Twitter (X) Reply Scraper return data in a format LLMs can use directly?

Yes. Typed, normalized JSON with consistent field names across runs, ISO-8601 UTC dates, integer counters and one predictable nesting level. No HTML parsing, no selectors, no relative-date strings to resolve. Pass a row straight into an LLM context window, index it into a vector store, or hand it to an agent tool.

#### What happens when X changes its layout or anti-bot system?

The scraper is maintained, and the twelve-key output shape stays stable on your side regardless of what changed upstream. Two design choices reduce the exposure: the GraphQL query IDs are discovered from X's live front-end bundle on every run rather than hardcoded, with a known-good pair as a fallback, and the per-request transaction header is generated the same way X's own web app generates it. Pushback is handled as escalation rather than failure — datacenter proxy, then residential, with the query IDs and transaction generator rebuilt on the new IP. What no scraper can absorb for you is your session expiring: if X invalidates the cookie, refresh it from your browser and re-run.

#### Which X reply fields work best for AI training data and RAG indexing?

For RAG indexing: `tweetContent` on the post plus every `repliesData[].tweetContent` carry the entire information payload, and they chunk cleanly as one document per thread, with `tweetLink` as the citation anchor and `handle` as filterable metadata. For training data and analytics: `tweetDate`, `likeCount`, `retweetCount`, `quoteCount` and `commentCount` are the most structurally consistent fields across records, since they are populated on every row and on every reply. Treat `verified` carefully in any feature set — it is `true` or `null`, never `false`. All values return as typed primitives or plain nested objects, so no normalization pass is needed before indexing.

### 🔗 Related scrapers

| Scraper Name | What it extracts |
| ----- | ----- |
| Twitter / X Posts Scraper | A specific account's public post timeline, by URL, handle or user ID — no cookies needed |
| X (Twitter) Posts Search | Posts by keyword, hashtag, search URL or profile, with Top/Latest modes, time windows, media and author stats |
| Twitter Trends Scraper | Live and hourly trending topics for 62 countries plus worldwide, with board rank — no login needed |
| LinkedIn Post Comments Scraper | Comment threads on LinkedIn posts with author identity and reaction counts |
| Instagram Comment Leads Scraper | Instagram comment threads with commenter identity |
| TikTok Comments Scraper By Real Comment Filter | TikTok video comments with spam filtering |

### 💬 Your feedback

Found a bug, or a field X exposes on a reply that is not in the output — media, view counts, deeper thread levels? Open an issue on the Actor's Issues tab. Reports that include the exact input JSON and the search URL you used are the fastest to reproduce and fix.

# Actor input Schema

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

📋 Add one or more X search page links (the same URLs you get when you search on x.com). Each link defines what topic or keywords you want to follow. You can add as many as you need — great for tracking several topics in one run! 🌍✅

## `maxSearchResults` (type: `integer`):

🎯 Choose how many posts you want from each search link — higher numbers give you a broader snapshot; lower numbers finish faster. Pick what fits your project! ⚡📈

## `maxReplies` (type: `integer`):

💡 For each post found, decide how many replies to keep in your results. Ideal when you want a taste of the conversation or a deeper thread — you’re in control! 🙌🧵

## `authToken` (type: `string`):

🔑 Your X account session token (cookie `auth_token`), or set the AUTH\_TOKEN env var. Stored as a secret.

## `ct0` (type: `string`):

🛡️ The `ct0` cookie that pairs with your auth token (also sent as the x-csrf-token header), or set the CT0 env var. Stored as a secret.

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

🌍 Optional **Apify Proxy** settings for smoother runs in the cloud. Turn it on if you prefer routing traffic through Apify’s network — handy for stable jobs and privacy-friendly setups. Off by default; enable when it suits you! ☁️✅

## Actor input object example

```json
{
  "startUrls": [
    "https://x.com/search?q=%23journorequest+min_replies%3A10&src=typed_query&f=live"
  ],
  "maxSearchResults": 10,
  "maxReplies": 5,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Posts found for each search URL, with their direct replies nested under repliesData. Export as JSON/CSV/Excel or fetch via API.

# 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 = {
    "startUrls": [
        "https://x.com/search?q=%23journorequest+min_replies%3A10&src=typed_query&f=live"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("scraper-engine/twitter-x-reply-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 = {
    "startUrls": ["https://x.com/search?q=%23journorequest+min_replies%3A10&src=typed_query&f=live"],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("scraper-engine/twitter-x-reply-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 '{
  "startUrls": [
    "https://x.com/search?q=%23journorequest+min_replies%3A10&src=typed_query&f=live"
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call scraper-engine/twitter-x-reply-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scraper-engine/twitter-x-reply-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/TbMYNpdJ1VXC1RvVz/builds/KJNqbIDOJHVKxaoSG/openapi.json
