Twitter X Reply Scraper
Pricing
from $4.99 / 1,000 results
Twitter X Reply Scraper
🐦 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.
Pricing
from $4.99 / 1,000 results
Rating
0.0
(0)
Developer
Scraper Engine
Maintained by CommunityActor stats
0
Bookmarked
17
Total users
0
Monthly active users
21 days ago
Last modified
Categories
Share
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 intoauthTokenct0— the CSRF token that pairs with it, pasted intoct0
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,
ct0goes again as thex-csrf-tokenheader, and the request must also carry X's web bearer token and itsOAuth2Sessionauth-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.
SearchTimelineandTweetDetaillive at URLs containing an opaque per-operation ID. The Actor discovers both at runtime — it reads x.com to find the currentmain.<hash>.jsbundle, 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-idheader. X's web app derives a per-request transaction ID from its home page and itsondemand.jsfile. 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 orTweetWithVisibilityResultswrappers, 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
Bottomcursor, ashowMoreThreadscursor, 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
errorsarray. 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.
- Open Twitter (X) Reply Scraper on Apify and click Try for free
- Paste one or more x.com search URLs into Start URLs (
startUrls). The form arrives withhttps://x.com/search?q=%23journorequest+min_replies%3A10&src=typed_query&f=livepre-filled as a working example - Set Maximum posts to collect (
maxSearchResults) and Maximum replies per post (maxReplies) - Paste your
auth_tokeninto Session — auth token (authToken) and yourct0into Session — security (ct0). The run stops without both - Leave the proxy off to begin with — the Actor escalates on its own when X pushes back
- 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, 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
#journorequestsearches onmin_replies:10readstweetContentfor the request itself, then worksrepliesData[].handleandrepliesData[].tweetDateto 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[].fullnameandrepliesData[].verified, sorted onrepliesData[].likeCountso the replies the audience actually agreed with come first. - 📊 A social listening analyst tracking reaction to a launch pulls every reply's
tweetContentandtweetDateacross a week of searches, and checkscommentCountagainst the length ofrepliesDataon 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
repliesDataentries bylikeCountandretweetCountand filters onverified, turning a 400-reply thread into the ten accounts whose reply travelled. - 🤖 An AI engineer building a social-listening agent indexes the post's
tweetContenttogether with eachrepliesData[].tweetContentas one document per thread, so the agent answers "how did people respond to this" from the actual conversation rather than a summary, withtweetLinkas 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:
maxRepliescannot fetch more than one conversation page. The Actor makes exactly one reply request per post and does not paginate the conversation, somaxRepliestrims 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 to0— 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
maxSearchResultsfalls back to10;maxRepliesfalls back to5. Values above the maximum are clamped to 5000 and 500 respectively, and values below 1 are raised to 1 — matching the schema bounds exactly. maxSearchResultsis 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.
urlsis accepted in place ofstartUrls, andauth_tokenin place ofauthToken. They work, but the schema names are the ones the Console form writes.
Example input
{"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
{"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:
{"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
from apify_client import ApifyClientclient = 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.