Twitter/X Posts Scraper avatar

Twitter/X Posts Scraper

Pricing

$19.99/month + usage

Go to Apify Store
Twitter/X Posts Scraper

Twitter/X Posts Scraper

Scrapes posts from Twitter X profiles, hashtags, or searches, capturing tweet text, media, timestamps, metrics, authors, hashtags, and URLs. Ideal for trend research, competitor analysis, sentiment studies, influencer insights, and large-scale Twitter X post extraction.

Pricing

$19.99/month + usage

Rating

5.0

(3)

Developer

Scraper Engine

Scraper Engine

Maintained by Community

Actor stats

3

Bookmarked

148

Total users

4

Monthly active users

47 days

Issues response

18 days ago

Last modified

Share

Twitter X Posts Scraper — Post Text, Engagement and Author Data

Twitter X Posts Scraper extracts posts from public X (formerly Twitter) profiles into flat, structured JSON: post text, permalink, ISO timestamp, likes, replies, reposts, quotes, views and bookmarks, plus the author's handle, display name, bio, follower counts and verification flag on every row. Photos, video URLs, hashtags, tagged users and quoted posts come along too. Paste handles or profile URLs, set a post limit, press Start — no login, no cookie, no parsing.

⚠️ Three things worth knowing before your first run. maxTweets is a ceiling per profile, and the schema caps it at 100. sortOrder is applied after collection, so it re-ranks the batch you already pulled rather than asking X for a different slice — see the section below. And individual post URLs are accepted by the input field but skipped at runtime: this Actor reads profile timelines only.

What is Twitter X Posts Scraper?

Twitter X Posts Scraper is an Apify Actor that reads the public post timeline behind any X profile and returns one flat row per post — 25 keys, the same keys every run. It resolves your handle to X's numeric user id, then pages through X's own timeline GraphQL operation with cursors until it reaches your limit or the timeline runs out.

No X account, login, cookie or session is used. The Actor harvests X's public guest bearer token from x.com and activates a guest token, so every request it sends is a logged-out request. There is no auth input in the schema, and nothing to paste.

It is built for social media analysts benchmarking accounts, brand and PR teams monitoring public handles, research teams building post-level datasets, and developers and AI engineers piping post text and engagement metrics into dashboards, RAG indexes or agent tools.

What X post data is publicly available to scrape?

X serves a profile's post timeline to logged-out guest clients: post text, id, permalink, creation time, the full engagement counter set and the author's public profile block all arrive in that payload. What needs a real logged-in session is keyword search and anything conversational.

Data CategoryAvailable logged-outRequires a logged-in X session
Post text, id, permalink, creation time✅ Public
Likes, replies, reposts, quotes, bookmarks, views✅ Public
Author handle, display name, avatar, bio, profile link✅ Public
Author follower, following and total-post counts, verification flag✅ Public
Photos and MP4 video variants attached to a post✅ Public
Quote-tweet target — text, id, author name✅ Public
Keyword and hashtag search resultsLogged-in session — X answers guest search requests with an empty 404
Replies to a postLogged-in session — a separate Actor with auth_token + ct0
Posts on a protected (private) accountLogged-in account the owner has approved
Who liked or reposted, DMs, post analyticsNot present in the timeline payload this Actor reads

Twitter X Posts Scraper only returns publicly visible data — what any logged-out visitor sees on a profile timeline. Nothing behind a login wall.

⚠️ sortOrder ranks what was collected, not X's whole timeline

This is the one input whose name promises more than X allows, so it is worth stating plainly rather than burying it in the parameter table.

X's profile timeline endpoint takes no sort parameter. It returns posts newest-first and that is the only order on offer. sortOrder is therefore applied client-side, after collection and after truncation to maxTweets:

  • recent — sorts the collected batch by date_posted descending. This matches the order X already returned, so it is effectively a no-op.
  • oldest — sorts the collected batch by date_posted ascending. That gives you the oldest of the most recent maxTweets posts, not the account's oldest posts. With maxTweets capped at 100, an account's early history is not reachable through this Actor at all.
  • popular — sorts the collected batch by likes descending. That gives you the most-liked of the most recent maxTweets posts, not the account's all-time top posts.

Nothing is dropped or hidden by the choice — all three values return the same set of rows in a different order. Just read popular and oldest as "rank my batch", not "query X differently".

What data can I extract with Twitter X Posts Scraper?

Every post row carries four groups of data: what the post says, how people engaged with it, who published it, and what it links to or quotes.

Field NameDescription
idX's numeric post id, as a string (rest_id)
urlPost permalink, built as https://x.com/{user_posted}/status/{id}
user_postedAuthor's handle (screen name), without the @
nameAuthor's display name
descriptionFull post text (X's full_text)
date_postedCreation time as an ISO-8601 UTC string, e.g. 2026-07-21T13:04:11.000Z
likesLike count (X's favorite_count)
repliesReply count
repostsRepost count (X's retweet_count)
quotesQuote-tweet count
viewsView count, passed through from X's views.count without casting
bookmarksBookmark count
is_verifiedAuthor's blue-check flag (X's is_blue_verified)
followersAuthor's follower count
followingNumber of accounts the author follows
posts_countAuthor's lifetime post count (X's statuses_count)
profile_image_linkAuthor's avatar image URL
biographyAuthor's bio text
external_urlThe website link on the author's profile — not a link inside the post. null when the profile has none
hashtagsArray of hashtag texts in the post, without #. null when the post has none
tagged_usersArray of handles mentioned in the post. null when there are none
photosArray of full-size photo URLs. null when the post has no photos
videosArray of MP4 URLs for videos and animated GIFs, sorted highest bitrate first. null when the post has no video
quoted_postObject, always present — see the seven sub-keys below
inputObject with a single url key: the post's own permalink with a trailing slash

Post, author and identity fields

id and url identify the post; user_posted and name identify who wrote it. description is the complete post text as X returns it, so a long-form post arrives whole rather than clipped at the old 280-character boundary.

Nine author-level fields are flattened onto every single post row rather than nested or returned once per profile: name, user_posted, biography, external_url, followers, following, posts_count, profile_image_link and is_verified. That makes each row self-describing — drop it straight into a dataframe or a vector store without a join — at the cost of repeating the same author block on every post from that account. Two notes on those fields: is_verified is X's is_blue_verified, which is the paid blue check rather than the retired legacy verification, and external_url is the link on the author's profile, so it is identical on every row from that account and null for accounts with no website set.

input is a nested object with one key, url. Despite the name it does not carry the entry you supplied in startUrls — it is the post's own permalink with a trailing slash, so input.url and url differ only by that slash. There is no field that traces a row back to which startUrls entry produced it; use user_posted for that.

Engagement and count fields

likes, replies, reposts, quotes, bookmarks and views are the six counters, taken from X's timeline payload at the moment of the request. views is the odd one out: it is passed through from X's views.count with no cast, and the dataset view renders it as text for that reason — coerce it before doing arithmetic, and expect null on posts old enough to predate view counts.

Every counter is a snapshot, not a series. Two runs a week apart give you two points; the Actor stores no history of its own.

Media, hashtag and quoted-post fields

photos, videos, hashtags and tagged_users are null rather than [] when a post has none of that thing. Code against row["photos"] or [] rather than assuming a list. videos holds direct MP4 URLs pulled from X's video variants and sorted highest-bitrate-first, so videos[0] is the best available quality. Media URLs point at X's CDN and can expire.

quoted_post is always present as an object with exactly these seven keys:

Sub-keyDescription
data_postedQuoted post's creation time, ISO-8601 UTC. Note the spellingdata_posted, not date_posted
descriptionQuoted post's full text
post_idQuoted post's numeric id
profile_idQuoted post author's numeric user id
profile_nameQuoted post author's display name
urlAlways null — not built by this Actor
videosAlways null — not extracted for quoted posts

On an ordinary post all seven are null. On a quote-tweet, five fill and url/videos stay null. There is a third case: when X reports a quoted id but does not hydrate the quoted post — a deleted or protected original — only post_id fills and the rest stay null. data_posted is a genuine typo in this output shape; it is documented as-is rather than silently corrected, because other Actors publishing the same 25-key shape spell it date_posted. Map it if you are migrating.

Reposts are worth one sentence: a repost comes back as a row whose author is the account you scraped, with X's RT @handle: text in description. Only quote-tweets get a quoted_post object — there is no retweeted_post field. And because only the timeline's top-level entries are parsed, the grouped conversation modules X uses for self-reply threads contribute nothing.

🤖 Add-on: Need additional X data?

Post rows carry reply counts, not the replies themselves, and this Actor reads profile timelines only. X (Twitter) Posts Search covers keyword, hashtag and search-URL discovery when you do not have a handle to start from. Twitter (X) Reply Scraper takes post URLs and returns the conversation underneath them. For what is spiking rather than what one account said, Twitter Trends Scraper returns trending topics by country.

How does Twitter X Posts Scraper differ from the official X API?

X publishes API v2 with documented endpoints and terms, and it is the supported route for production work you need to contract around. The gap this Actor fills is access: API v2 requires a developer account, a project and an app, and its access tiers are paid.

FeatureX API v2Twitter X Posts Scraper
Developer account, project and app✅ Required❌ Not required
Access tierPaid tiers — verify current terms in X's published API documentationRuns on Apify, billed per saved post row
Bearer token or OAuth credential✅ Required❌ None — no X credential is accepted or needed
Reading a third party's public postsSupported within your tier's documented scope✅ Any public handle, profile URL or numeric user id
Author profile fields alongside each postRequested via expansions and field parameters✅ Flattened onto every row by default
Rate limits and quotasGoverned by X's published per-endpoint limitsBounded by this Actor's per-profile maxTweets cap and X's guest-endpoint behaviour
Output shapeX's versioned API schema25 flat keys, identical every run

Every constraint above should be verified against X's current published API documentation before you choose — tiers, scopes and endpoint limits change, and no prices, quotas or rate-limit figures from them are reproduced here.

Use API v2 when you need contractual guarantees, write access, or the streaming and filtered-stream endpoints. Use Twitter X Posts Scraper for read-only post and engagement collection across accounts you have no relationship with, without a developer application.

How to use Twitter X Posts Scraper

Twitter X Posts Scraper runs on Apify. Start it from the Apify Console or call it through the Apify API.

  1. Open Twitter X Posts Scraper on Apify and click Try for free
  2. Add your targets to Twitter / X Profile URLs or Usernames (startUrls) — the only required input. Full profile URLs, bare handles, @handles and numeric user ids all work, and you can mix them in one list
  3. Set Max Tweets per User (maxTweets) — a per-profile ceiling, 1 to 100
  4. Pick a Sort Order (sortOrder) if you want the batch re-ranked: recent, popular or oldest
  5. Turn on Proxy Configuration (proxyConfiguration) if you see blocks, timeouts or empty results — residential is the tier X's guest endpoints tolerate best
  6. Click Start, then export the dataset as JSON, CSV, Excel or XML

Profiles are processed sequentially, one after another, and each profile's rows are written as soon as that profile finishes — you can start reading results before the run ends.

How to scale to bulk post extraction

startUrls is a list, so one run covers as many profiles as you need. Plain strings are accepted and so are { "url": "..." } objects, which means output from another Actor can usually be fed in unchanged.

maxTweets applies per entry, not as a run total: twenty handles at maxTweets: 100 is a ceiling of 2,000 rows, not 100. A profile that cannot be resolved contributes no rows and the run moves on to the next one, so a single bad handle does not end the run. For recurring collection, put the same input on an Apify schedule and attach a webhook so each finished dataset lands in your own store.

What can you do with X post data?

  • 📊 A social media analyst benchmarking accounts pulls description, likes, reposts and views across a competitor set, then ranks post formats by engagement per view to decide what to brief next month.
  • 🏷️ A brand monitoring team watching mentions filters rows whose tagged_users contains their handle and reads date_posted and replies to spot a conversation building before it peaks.
  • 📈 A growth researcher tracking an account runs the same input weekly and stores followers and posts_count alongside date_posted, turning repeated snapshots into a posting-cadence-versus-growth series.
  • 🎬 A creative strategist auditing media performance splits rows by whether photos or videos is non-null and compares median likes in each bucket to settle the image-versus-video argument with data.
  • 🔗 A conversation researcher collects url for every post above an engagement threshold and feeds those permalinks into a dedicated X reply Actor, so only the threads worth reading get scraped.
  • 🤖 An AI engineer building a market-intelligence agent indexes description with url, user_posted and date_posted as metadata in a vector store, so the agent can answer "what has this company said about pricing this quarter, and which of those posts landed" against live public posts rather than a stale export.

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

How does Twitter X Posts Scraper handle rate limits and blocking?

The Actor does not solve CAPTCHAs. It gets through in three other ways: a real bearer token, a guest token, and an escalating proxy ladder.

Token harvest. At run start a headless Chromium loads x.com once, purely to intercept the public Bearer authorization header X's own web app sends, and a guest token is then activated against api.x.com/1.1/guest/activate.json. Both are public artefacts of the logged-out web client — no credential of yours is involved. Every subsequent request carries a full Chrome desktop header set. If no bearer token can be captured, the run logs an error and exits before pushing any rows, rather than returning empty results.

Egress. By default there is no Apify proxy: requests leave directly from the Apify run. If you set useApifyProxy, one proxy URL is resolved at run start and used for guest-token activation, user-id resolution and every timeline call — sticky egress for the whole run, not per-request rotation. The bearer harvest deliberately runs without the proxy, because that token is IP-agnostic and a residential hop only slows it down. The Actor's own source notes that X blocks datacenter and hosting IPs for logged-out guest access, which is why residential is the tier to pick if you enable a proxy at all.

Escalation. If a request raises an exception, the Actor climbs the ladder: no proxy → Apify datacenter (SHADER) → Apify residential, retrying residential up to three times, then sticking with whatever tier worked for the rest of the run. One honest caveat: the timeline fetch catches its own HTTP errors and returns nothing rather than raising, so a plain non-200 block from X ends that profile's pagination instead of triggering the ladder. That is the practical reason to enable proxyConfiguration up front rather than relying on the fallback.

When a profile fails, it contributes no rows and the run continues to the next entry. Nothing partial or placeholder is written, and because charging is per saved post row, a failed profile costs nothing.

⬇️ Input

Four parameters, one of them required. startUrls is the only thing you have to supply.

ParameterRequiredTypeDescriptionExample Value
startUrlsYesarrayX profile targets, one per line. Accepts full profile URLs (x.com/... or twitter.com/...), bare handles, @handles and numeric user ids, mixed freely. Plain strings and { "url": "..." } objects are both accepted. Each entry pulls that account's public post timeline.["https://x.com/NASA", "@Interior", "elonmusk", "44196397"]
sortOrderNostringOrder applied to the collected batch before it is saved. One of recent, popular, oldest. Default "recent". Applied client-side after collection — see the section above."popular"
maxTweetsNointegerMaximum posts to collect per entry, not per run. Minimum 1, maximum 100. Default 10.50
proxyConfigurationNoobjectApify Proxy settings. No Apify proxy by default. Residential is the tier X's guest endpoints tolerate best.{"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}

Four honest notes on those four parameters.

  • Post URLs are accepted but skipped. startUrls takes any string, and an x.com/{handle}/status/{id} URL is recognised — then deliberately dropped, with a log line telling you to supply a handle or user id instead. It contributes no rows. Individual posts are not this Actor's entity; profile timelines are.
  • Search URLs are not merely unsupported, they are misread. An x.com/search?q=... or x.com/hashtag/... URL falls through the same path-segment parser as a profile URL, so it is treated as the handle search or hashtag and resolves to the wrong account or to nothing. For keyword and hashtag discovery use the search-driven sibling Actor listed below.
  • Send maxTweets explicitly when you call the Actor from code. The schema declares default: 10 and maximum: 100, which is what the Console form gives you. The Actor's own fallback, used when the key never reaches it, is 100 — and it then clamps to 1000, a ceiling the schema's maximum: 100 means you cannot reach anyway. Passing the value explicitly removes the ambiguity.
  • Pagination is 20 posts per request. The Actor asks X for min(20, maxTweets) posts per call and follows the cursor until your limit is reached or X stops returning one. That page size is X's, not a setting.

Example input

{
"startUrls": [
"https://x.com/NASA",
"https://twitter.com/Interior",
"@NWSNHC",
"elonmusk",
"44196397"
],
"sortOrder": "popular",
"maxTweets": 50,
"proxyConfiguration": {
"useApifyProxy": true,
"apifyProxyGroups": ["RESIDENTIAL"]
}
}

⬆️ Output

Every post is one dataset row with the same 25 keys, run after run, in a stable order. Rows are written as each profile finishes, so the dataset fills while the run is still going. Export as JSON, CSV, Excel or XML, or read the dataset through the Apify API.

Three shape rules to code against. First, all 25 keys are always present — nothing is omitted, so no presence check is needed; missing values arrive as null. Second, hashtags, tagged_users, photos and videos are null rather than [] when empty. Third, quoted_post is always an object with the same seven keys, all null on a post that quotes nothing — and its date key is spelled data_posted.

Every row in the dataset is a post. The Actor writes no error rows, no status rows, no diagnostic rows and no run-summary rows, so there is no errorReason, status or isError marker to filter on and nothing to exclude — a failed profile simply produces fewer rows. One row_result event is charged per saved post row, so a profile that returns nothing costs nothing.

Two behaviours worth planning for. There is no de-duplication step: rows go out exactly as the timeline yields them, so listing the same handle twice in startUrls — or X returning the same post as both a pinned entry and a chronological one — produces duplicate rows and duplicate charged events. De-duplicate on id downstream if that matters. And the run log prints a red Failed to push data line for rows that were in fact saved, because the Apify SDK returns nothing on a successful push; trust the dataset row count over that log line.

Example output

A photo post with hashtags, from a profile that quotes nothing:

{
"biography": "There's space for everybody.",
"bookmarks": 1284,
"date_posted": "2026-07-21T13:04:11.000Z",
"description": "Our Webb telescope just returned its deepest look yet at the Sagittarius C star-forming region. Thousands of protostars, packed into a slice of sky narrower than a grain of rice held at arm's length. #Webb #Astronomy https://t.co/9xKpQ2mZ4t",
"external_url": "https://t.co/GBqTWZTAcM",
"followers": 79412663,
"following": 291,
"hashtags": ["Webb", "Astronomy"],
"id": "1947284310256119842",
"input": {
"url": "https://x.com/NASA/status/1947284310256119842/"
},
"is_verified": true,
"likes": 41827,
"name": "NASA",
"photos": [
"https://pbs.twimg.com/media/G0aQ1fLXkAA8ZpT.jpg"
],
"posts_count": 71104,
"profile_image_link": "https://pbs.twimg.com/profile_images/1321163587679784960/0ZxKlaBf_normal.jpg",
"quoted_post": {
"data_posted": null,
"description": null,
"post_id": null,
"profile_id": null,
"profile_name": null,
"url": null,
"videos": null
},
"quotes": 312,
"replies": 1046,
"reposts": 7395,
"tagged_users": null,
"url": "https://x.com/NASA/status/1947284310256119842",
"user_posted": "NASA",
"videos": null,
"views": 3184092
}

A quote-tweet with a video from the same run — note the five filled quoted_post sub-keys, the two that stay null by design, and the bitrate-sorted videos array:

{
"biography": "Protecting America's natural resources and heritage.",
"bookmarks": 96,
"date_posted": "2026-07-20T16:41:52.000Z",
"description": "This is what a healthy river restoration looks like four years on. Elwha River, @OlympicNP. https://t.co/7bVn4dQ1sW",
"external_url": "https://t.co/2GLmDqPfNi",
"followers": 2417803,
"following": 447,
"hashtags": null,
"id": "1946951773094820511",
"input": {
"url": "https://x.com/Interior/status/1946951773094820511/"
},
"is_verified": true,
"likes": 5218,
"name": "US Department of the Interior",
"photos": null,
"posts_count": 24880,
"profile_image_link": "https://pbs.twimg.com/profile_images/1509923297428480003/QpJfMWNu_normal.jpg",
"quoted_post": {
"data_posted": "2026-07-19T21:15:03.000Z",
"description": "Four years since the last of the Elwha dams came down. The salmon are back above the old dam site for the first time in a century.",
"post_id": "1946662104913772210",
"profile_id": "88972735",
"profile_name": "Olympic National Park",
"url": null,
"videos": null
},
"quotes": 74,
"replies": 188,
"reposts": 1203,
"tagged_users": ["OlympicNP"],
"url": "https://x.com/Interior/status/1946951773094820511",
"user_posted": "Interior",
"videos": [
"https://video.twimg.com/ext_tw_video/1946951620117401600/pu/vid/avc1/1280x720/hQ8mZk2Tn1pWx0Ab.mp4",
"https://video.twimg.com/ext_tw_video/1946951620117401600/pu/vid/avc1/640x360/Lk3rMv8QcTd4pXeY.mp4"
],
"views": 742615
}

How does it work?

X's own web app reads a profile timeline from a GraphQL operation, and that is what this Actor calls. Each run begins by loading x.com once in headless Chromium purely to intercept the public Bearer token the web client sends, then activates a guest token — the two credentials a logged-out browser session uses. From there no browser is needed.

For each entry it normalises your input into a handle, a numeric user id, or a post URL it rejects. A handle is resolved to X's numeric user id through the guest-accessible UserByScreenName operation over plain HTTP, with a headless-browser interception as a second attempt if that call comes back empty. It then calls the UserTweets operation page after page, following X's bottom cursor until your maxTweets limit is reached or no cursor comes back. Each post is parsed out of the structured timeline entry, the batch is ordered by your sortOrder, and rows are written to the dataset.

Because the Actor reads structured GraphQL responses rather than rendered markup, an X front-end redesign generally does not affect it, and your field names stay put. Only publicly visible posts are collected — no account, cookie or session is used anywhere in the run.

Integrations

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

Calling Twitter X Posts Scraper from Python

from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("<YOUR_USERNAME>/twitter-x-posts-scraper").call(run_input={
"startUrls": ["https://x.com/NASA", "@Interior", "44196397"],
"sortOrder": "popular",
"maxTweets": 50,
"proxyConfiguration": {"useApifyProxy": True, "apifyProxyGroups": ["RESIDENTIAL"]},
})
for post in client.dataset(run["defaultDatasetId"]).iterate_items():
print(post["date_posted"], post["likes"], post["user_posted"], post["description"][:80])
for photo in post["photos"] or []:
print(" photo:", photo)

Works in Go, Ruby, Node.js, cURL — any language that can make an HTTP request. Two habits pay off: guard the array fields with or [], because they are null when empty, and pass maxTweets explicitly rather than relying on a default. The Actor id is the lowercase, hyphenated username/actor-name pair shown on the Actor's page — copy it from there rather than typing the display name.

Scheduling and webhooks

Put the same input on an Apify schedule for recurring collection — a daily pull across a competitor set is one cron entry with maxTweets sized to how often those accounts post. Attach an Apify webhook on run success and each finished dataset is posted to your endpoint, so new posts land in your warehouse without polling. Because engagement counters are snapshots, scheduled runs are also how you build a time series: keep id, date_posted and the counters from each run and diff them.

No-code tools (n8n, Make, Zapier)

In n8n, use the Apify node — or an HTTP Request node pointed at the Apify run endpoint with your token — and pass the same JSON input shown above; an Item Lists node then splits posts into individual rows for downstream steps. In Make, the Apify module supports run-and-wait, so a daily handle monitor can feed a Google Sheets, Airtable or Slack step directly. Both platforms handle scheduling, so a recurring account sweep needs no code at all.

Scraping publicly visible posts is broadly treated as permissible where no authentication is bypassed, and Twitter X Posts Scraper returns only public posts — what any logged-out visitor sees on a profile timeline. No login, no cookie, no protected account. But this output contains personal data, and that shifts obligations onto you rather than onto the Actor.

The personal-data fields are specific and worth naming: user_posted, name, profile_image_link, biography and external_url identify a real person, or the person behind an account; description is text that person authored; tagged_users carries the handles of third parties who did not post and never chose to be in your dataset; followers, following, posts_count and is_verified profile that account; and id, url, quoted_post.profile_id and quoted_post.profile_name are persistent identifiers linking straight back to an individual.

Under GDPR, UK GDPR and CCPA that means:

  • Lawful basis. Public availability is not one. If you rely on legitimate interest, run and write down a balancing test: your purpose, why less intrusive means will not do, and the effect on the people whose posts you stored. Keep it on file before you collect at scale, not after a complaint.
  • Data minimisation. Keep only the fields your use case needs. Drop profile_image_link, biography, external_url and tagged_users first — they are rarely load-bearing for analytics and they are the most intrusive. For pure engagement analysis, id, date_posted, likes, replies, reposts, quotes, bookmarks and views carry no personal data at all once the author fields are gone.
  • Retention. Set a limit and enforce it. Counters are snapshots, so old rows lose analytical value long before they stop carrying risk.
  • Transparency and subject requests. If you process this data about identifiable people, be prepared to tell them and to honour access, correction and deletion requests against everything you actually store — including derived tables and vector indexes, which are the copies teams forget.

Consult legal counsel if your use case involves bulk storage of personal data, profiling individuals, building datasets about identifiable people, or republishing post text and author identities.

❓ Frequently asked questions

What X post fields does Twitter X Posts Scraper return?

The five most used are description, likes, date_posted, user_posted and url. Twenty-five keys come back on every row — six engagement counters, nine author fields, media arrays for photos and videos, and a seven-key quoted_post object. See the data fields table above for all of them.

No — and there is no input to paste one into. The schema has exactly four parameters (startUrls, sortOrder, maxTweets, proxyConfiguration) and none of them is a credential. The Actor harvests X's public guest bearer token from x.com and activates a guest token, so every request is a logged-out request. That is a real difference from X reply and keyword-search scrapers, which need your own auth_token and ct0 cookies to return anything at all. The one credential you need is your Apify token. If a run does fail at the token step, the log line mentions being logged in — ignore that wording; the Actor never logs in, and the real cause is that x.com did not serve a bearer token on that attempt. Retry, with a residential proxy enabled.

How many posts can I extract in one run?

maxTweets sets the ceiling per entry, from 1 to 100, so a run's total is up to maxTweets × the number of entries in startUrls. There is no cap on how many profiles one run can take. Two things sit under your number: X returns at most 20 posts per timeline request, and pagination stops early when X stops returning a cursor — which is what happens on accounts that have posted less than you asked for.

What happens if an account is protected, suspended, deleted or has no posts?

The entry contributes no rows and the run continues to the next one. For a protected, suspended or non-existent handle, X's guest lookup returns no numeric user id; the Actor logs that the account may be suspended or protected and moves on. For an account that exists but has posted nothing, the id resolves, the first timeline page comes back with no post entries, and the profile ends with zero rows. In all of these cases nothing partial or fabricated is written — the dataset contains only real posts, so an unreachable account reduces your row count rather than corrupting it. Since charging is per saved post row, that entry costs you nothing. In code, detect it by absence: no row carries that handle in user_posted.

Can I scrape multiple X profiles at once?

Yes. startUrls is a list — add as many profile URLs, handles, @handles and numeric user ids as you like, and mix the formats freely. Profiles are processed sequentially and maxTweets applies to each one independently. Two caveats: individual post URLs are skipped rather than scraped, and there is no de-duplication, so the same handle listed twice produces two sets of identical rows.

Can I scrape a single post, a keyword or a hashtag with this Actor?

No — this Actor reads profile timelines only. A post URL in startUrls is recognised and then skipped with a log line. An x.com/search?q=... or x.com/hashtag/... URL is worse than unsupported: it is parsed as though search or hashtag were a handle, so it resolves to the wrong account or to nothing at all. Use X (Twitter) Posts Search for keyword, hashtag and search-URL targets, and Twitter (X) Reply Scraper for the replies under a specific post.

Does Twitter X Posts 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.

How does Twitter X Posts Scraper compare to other X post scrapers?

Checked on the Apify Store on 25 July 2026: scraper_one/x-profile-posts-scraper is the closest match in purpose — profile-driven, with profileUrls, a resultsLimit its listing documents as 1–200 with a default of 5, and a skipPinnedPosts toggle. Its output shape is different: postText, postUrl, timestamp as epoch milliseconds, conversationId, and a nested author object rather than flattened author fields. Its listing documents conversationId and the pinned-post toggle, neither of which this Actor has; it does not document views, bookmarks, hashtags, tagged users or a quoted-post object. Its listing also states that free users are rate-limited to a few requests per day. scraper_one/x-posts-search is keyword- and hashtag-driven rather than profile-driven, with query, resultsCount, timeWindow/timeWindowHours and searchType, and carries the same rate-limit note. pratikdani/twitter-posts-scraper documents the same 25-key flat shape this Actor returns, field for field, with two documented differences inside the quoted object: its quoted_post spells the date key date_posted and also documents photos and a populated url, where this Actor spells it data_posted and leaves url and videos null. Its listing does not document its input parameters.

What this Actor documents that those listings do not: that it needs no X cookie or session at all, that sortOrder is a client-side re-rank rather than a different query to X, that the array fields come back null instead of [], that no error or accounting rows are ever written, and exactly which two quoted_post sub-keys are always null.

Does Twitter X Posts Scraper return data in a format LLMs can use directly?

Yes. Every row is typed, normalized JSON with the same 25 field names on every run. No HTML parsing, no selectors. Pass a row straight into an LLM context window, index it into a vector store, or hand it to an agent tool — the only transformation worth doing first is coercing views and replacing null arrays with empty ones.

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

The scraper is maintained, and because it reads X's structured GraphQL timeline responses rather than rendered HTML, a visual redesign generally does not affect it. The parts genuinely exposed to change are X's GraphQL query ids and feature flags, and the guest-access rules around them — which is exactly why the bearer token and guest token are harvested fresh from x.com on every single run rather than pinned. Your field names and types do not change on your end.

Can I use Twitter X Posts Scraper without managing proxies or browser infrastructure?

Yes. Chromium runs inside the Actor, only for the token harvest, and you never provision it. Proxying is a single checkbox: leave proxyConfiguration off for a direct connection, or enable Apify Proxy — residential is the tier X's guest endpoints tolerate best — and the Actor resolves one egress and reuses it for the whole run. You never create a proxy account or rotate an IP. It does not solve CAPTCHAs; it avoids challenges with a real browser-issued token, a guest token and browser-accurate headers, and escalates its proxy tier when a request errors out.

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

For RAG indexing: description is the document body, quoted_post.description adds the context a quote-tweet is responding to, and hashtags plus tagged_users make good filterable metadata — with url as the citation link and date_posted for recency ranking. For training data: likes, replies, reposts, quotes, bookmarks, followers, following and posts_count are the most structurally consistent values across records, since they are always present and always numeric. Everything returns as a typed primitive, array or plain nested object, so no normalization pass is needed beyond coercing views — and if your index does not need author identity, drop name, profile_image_link, biography and external_url before you store anything.

Scraper NameWhat it extracts
X (Twitter) Posts SearchPosts by keyword, hashtag, search URL or profile, with day and hour time windows and top/latest modes — keyword search needs your own X session cookies
Twitter (X) Reply ScraperReplies underneath specific X posts — needs your own auth_token and ct0
Twitter Trends ScraperTrending topics by country, live or by hour
Threads User Posts Scraper By Keyword & Date FilterThreads posts from a user, filtered by keyword and date
Truth Social Scraper With Photos & VideosTruth Social posts with their attached photos and videos
LinkedIn Post ScraperPost text, engagement and author data from LinkedIn
Facebook Posts ScraperPublic Facebook Page and profile posts with a full reaction breakdown

💬 Your feedback

Found a bug, or need a field that is in X's timeline payload but not in these 25 keys? Open an issue on the Actor's Issues tab. Reports that include the exact input JSON and the profile URL or handle you targeted are the fastest to reproduce and fix.