How to Scrape Twitter (X) With R (2026)
rtweetis archived (November 10, 2024) and the Twitter Academic Research Track was removed from all accounts in 2023, so the classic free R packages no longer return tweets.- Four R routes still work: the paid X API v2 called from
httr2, a headless browser viarvest::read_html_live()orRSelenium, or a managed scraper API that returns JSON you load into a tibble. - The wall is the same in R as every language: a logged-out
httr2orrvestGET onx.comreturns a JavaScript shell with zero tweets in the HTML. - For a stable feed I call a managed API from
httr2: one GET returns parsed JSON straight into a data frame, with the proxy and token work handled server-side.
The R question I get most is whether a package still scrapes Twitter the way rtweet used to. The honest answer to how to scrape Twitter (X) with R in 2026 is that the classic packages are gone, but R can still pull tweets, just not through the free API path every 2021 tutorial describes. rtweet was archived, X shut its free read API, and the academic track closed, so the working routes now are the paid X API called from httr2, a headless browser driven by rvest or RSelenium, or a managed scraper API that hands you JSON.
The wall is easy to show from R. When I request a profile logged out with httr2, X returns HTTP 200 and a large HTML shell with no tweets in it, because the timeline only exists after a browser runs the page scripts:
# install.packages(c("httr2", "stringr"))
library(httr2)
library(stringr)
html <- request("https://x.com/nasa") |>
req_user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0 Safari/537.36") |>
req_perform() |>
resp_body_string()
nchar(html) # ~607000 characters
str_count(html, fixed('data-testid="tweet"')) # 0
That empty result is the whole problem in one request, and it is identical whether you use rvest, httr2, or curl. This guide shows the R code I ran in July 2026 for each route that still returns data, where each one breaks, and the managed call I use when I want tweets as a data frame without babysitting tokens.
Can You Still Scrape Twitter (X) With R in 2026?
You can still scrape Twitter (X) with R in 2026, but the free packages that made it a two-line job are archived, so the working routes now split into four. Each one moves the hard part (authentication, proxies, or the moving JavaScript surface) somewhere different, and the right pick depends on your volume and budget.
| R route | What it needs | What you get | Holds up under change |
|---|---|---|---|
httr2 + official X API v2 | Paid X developer app, bearer token | Sanctioned JSON, metered per read | Stable, but priced per post |
Managed scraper API via httr2 | One API key | Parsed JSON into a tibble | Strong, provider absorbs X changes |
rvest::read_html_live() (chromote) | A local headless Chrome | Rendered DOM you parse yourself | Fragile, detectable, markup churns |
RSelenium / selenium browser | A running browser, ideally a login | Rendered tweets you parse yourself | Fragile, detectable, high upkeep |
The table is the whole decision in miniature: pay X per read, offload the fetch, or run a browser yourself. The rest of this guide walks each route in R, starting with why the package you probably came here to use stopped working.
Why Did rtweet and academictwitteR Stop Working?
rtweet and academictwitteR stopped working because X removed the free API access they were built on, and then the maintainers archived them. rtweet, the package every older tutorial recommends, was archived on the rOpenSci GitHub on November 10, 2024. You can still install.packages("rtweet"), but its search_tweets() and get_timeline() calls now need a paid X API app to return anything, which defeats the reason most people reached for it.
The academic route closed first. academictwitteR still sits on CRAN, but it targets the Twitter Academic Research Product Track, and X removed that track from every account in mid-2023. The package installs and its functions run, yet the endpoint behind them no longer exists, so a new project gets errors rather than tweets. The older twitteR package predates the v2 API entirely and has not been a serious option for years.
What replaced all of it is metered. X’s Developer Platform moved to pay-per-use pricing with no free read tier for a new project, so the httr2-against-the-API route is a real cost decision now, not a formality. Those numbers are the first thing to price before you write any official-API code.
How Do You Scrape Twitter With R Using the Official X API?
You scrape Twitter with R using the official X API by calling its v2 endpoints yourself with httr2, because rtweet is archived and no maintained package wraps the current paid endpoints cleanly. httr2 is the modern R HTTP client: you build one request() and add pieces with req_*() helpers, and req_auth_bearer_token() attaches the bearer token X issues for a developer app.
You store the token in .Renviron and call the recent-search endpoint like this:
# install.packages("httr2")
library(httr2)
# Requires a paid X API app; keep the token out of your script.
bearer <- Sys.getenv("X_BEARER_TOKEN")
resp <- request("https://api.x.com/2/tweets/search/recent") |>
req_url_query(query = "from:nasa", max_results = 20) |>
req_auth_bearer_token(bearer) |>
req_perform()
data <- resp_body_json(resp)
str(data$data)
With a valid paid app this returns the standard v2 JSON, and resp_body_json() hands it back as a nested R list you can flatten. The catch is the meter. The X API pricing page lists post reads at $0.005 per resource and other user reads at $0.010, with reads of your own account’s data at $0.001, and pay-per-use is capped at 2 million post reads a month before Enterprise is required. For a compliant, low-volume pull this is the sanctioned route. Past a few thousand reads the economics push most R users toward a browser or a managed API, and the managed call needs no X app at all.
How Do You Scrape Twitter With R Without an API Key or Browser?
You scrape Twitter with R without an X API key or a browser by sending the username or search query to a managed scraper API and reading the JSON it returns, while the provider handles guest tokens, the rotating query identifiers, and proxies. From R this is one request(), one req_perform(), and a parse into a data frame, which is the part R is actually good at.
The request is a plain GET with your key as a query parameter. This is the ChocoData shape I use, shown first as curl so it is language-neutral:
curl "https://chocodata.com/api/v1/twitter/profile?username=nasa&api_key=$CHOCO_API_KEY"
The same call in R is four lines, and resp_body_json() gives you the profile fields directly:
# install.packages("httr2")
library(httr2)
resp <- request("https://chocodata.com/api/v1/twitter/profile") |>
req_url_query(username = "nasa", api_key = Sys.getenv("CHOCO_API_KEY")) |>
req_perform()
profile <- resp_body_json(resp)$data
cat(profile$name, "-", profile$followers_count, "followers\n")
I ran the managed call from R in July 2026 to confirm the auth path. Sent with a deliberately invalid key, the request authenticated against api.chocodata.com and came back as 401 {"error":{"code":"INVALID_API_KEY","message":"Api key not recognised."}}, so the authentication step is real; with a valid key it returns the profile JSON with no login cookie and no proxy pool on my side. To collect a user’s recent tweets instead of the profile, change the resource to tweets and let httr2 simplify the array straight into a tibble you can write to CSV:
# install.packages(c("httr2", "dplyr"))
library(httr2)
library(dplyr)
resp <- request("https://chocodata.com/api/v1/twitter/tweets") |>
req_url_query(username = "nasa", count = 50, api_key = Sys.getenv("CHOCO_API_KEY")) |>
req_perform()
tweets <- resp_body_json(resp, simplifyVector = TRUE)$data$tweets
df <- as_tibble(tweets) |>
select(created_at, text, favorite_count, retweet_count)
write.csv(df, "nasa_tweets.csv", row.names = FALSE)
head(df)
That gives you a clean data frame with created_at, text, and engagement columns, ready for dplyr or ggplot2, without registering an X app or refreshing a token. Swapping the resource changes what you collect, and each maps to a dedicated endpoint on the same base and key:
| You want | Endpoint | Example query |
|---|---|---|
| A profile / account | Profile scraper | .../twitter/profile?username=nasa |
| Tweets / posts | Tweet scraper | .../twitter/tweets?username=nasa&count=50 |
| Search / hashtag | Search scraper | .../twitter/search?query=<terms> |
| Followers / following | Follower scraper | .../twitter/followers?username=nasa |
The free tier covers 1,000 requests, which is enough to wire up the httr2 calls above before you commit to anything. That handles the no-account routes. If you would rather render the page yourself in R, the browser options come next, starting with the lightest one.
How Do You Render JavaScript Tweets in R With rvest and chromote?
You render JavaScript tweets in R with rvest by calling read_html_live() instead of read_html(), which drives a real headless Chrome through the chromote package and gives you the DOM after X’s scripts run. Plain read_html() only ever sees the static shell from the intro, so it returns zero tweets; read_html_live() is the function in modern rvest that changed that.
# install.packages("rvest") # read_html_live() pulls in chromote
library(rvest)
session <- read_html_live("https://x.com/nasa")
# session$view() # optional: watch the headless browser render
Sys.sleep(5) # let the GraphQL timeline call resolve
session |>
html_elements('article [data-testid="tweetText"]') |>
html_text2() |>
head()
This is lighter than RSelenium because there is no separate Selenium server or driver process to stand up, just the chromote browser rvest starts for you. The catch is the one every browser route hits: a fresh, logged-out Chrome on a datacenter IP frequently lands on X’s login or challenge wall before the timeline renders, and the [data-testid="tweetText"] selectors break whenever X reshuffles its markup. For a handful of public profiles it is the quickest R render to stand up. For anything heavier or session-based, RSelenium gives you more control over the browser.
How Do You Scrape Twitter With R Using RSelenium?
You scrape Twitter with R using RSelenium by launching a real browser, loading an X session, and reading the rendered tweets from the page after its scripts run. RSelenium gives R bindings to the Selenium WebDriver, so it drives Firefox or Chrome the way a person would, and its rsDriver() helper manages the driver binaries for you.
A minimal profile pull looks like this:
# install.packages("RSelenium")
library(RSelenium)
driver <- rsDriver(browser = "firefox", port = 4567L)
remote <- driver$client
remote$navigate("https://x.com/nasa")
Sys.sleep(5) # let the GraphQL timeline call resolve
nodes <- remote$findElements(using = "css selector", 'article [data-testid="tweetText"]')
texts <- sapply(nodes, function(e) e$getElementText()[[1]])
print(head(texts))
remote$close()
driver$server$stop()
Two things make this the heaviest R route. First, a default WebDriver session carries a detectable fingerprint, so X often shows a login or challenge wall before the timeline, and it is steadiest when you load a logged-in session’s cookies rather than hitting the profile cold. Second, you run a full browser plus a driver process, which is slow for anything past a few hundred tweets. A newer, lighter selenium package now implements the W3C WebDriver protocol directly as an up-to-date alternative to RSelenium, but it inherits the same detection reality. The same browser approach works in other stacks too, and I walk through Node, PHP, and Go in my Selenium and other-language guide.
Which R Method Should You Choose to Scrape Twitter?
The right R method to scrape Twitter depends on volume, on whether you can pay for an X app, and on how much browser upkeep you want to own. Here is the summary I give people who ask, based on what returned data in my July 2026 R testing.
| If you need… | Use in R | Why |
|---|---|---|
| A compliant, low-volume pull, have a paid X app | httr2 against the X API v2 | Sanctioned, but metered per post read |
| Tweets as JSON at volume, no accounts or proxies | Managed scraper API via httr2 | Parsed JSON into a tibble; tokens and proxies server-side |
| To render a few public profiles, minimal setup | rvest::read_html_live() | Real headless Chrome from R; slower, still detectable |
| Maximum fidelity, willing to run a full browser | RSelenium or the newer selenium | Behaves like a real browser; slow, detectable, high upkeep |
The old free rtweet workflow back | Nothing equivalent | rtweet is archived and the free read API is gone |
Before you collect at volume, the legal line is worth knowing regardless of language. Scraping publicly visible pages is generally treated as lawful in the United States, but X’s Developer Policy restricts automated access, and its Terms set liquidated damages for large automated pulls, so the R you write does not change the legal position. I work through the CFAA and Terms detail in is scraping Twitter legal.
If you would rather compare finished tools than write and maintain scraper code, I rank the managed options head to head in my best Twitter scrapers in 2026 roundup, where a managed API led on success rate in my runs. For most R projects the choice comes down to a simple trade: pay X per read for the sanctioned route, accept the browser upkeep of rvest or RSelenium, or hand the fetch to an API and spend your R time on the analysis instead of the plumbing.
FAQ
Which R package replaces rtweet for scraping Twitter?
There is no single drop-in replacement for rtweet in 2026. For the sanctioned route you build requests against the paid X API v2 yourself with httr2; for a browser approach you use rvest::read_html_live() or RSelenium; and for tweets as JSON without accounts or proxies you call a managed scraper API from httr2. rtweet itself was archived in November 2024 and the free API it relied on is gone.
Does rvest work for scraping Twitter (X)?
Plain rvest::read_html() does not, because it only fetches the static HTML shell and X loads the tweets afterward with JavaScript, so you get zero tweet nodes. The newer rvest::read_html_live() does render the page: it drives a headless Chrome through the chromote package and returns the DOM after the scripts run. It still meets X's login or challenge wall on a fresh logged-out session, so it suits a few public profiles, not a large feed.
Can you scrape Twitter with R without a developer account?
Yes. You can skip the X developer account by rendering the page with rvest::read_html_live() or RSelenium and reading the tweets, or by sending a username or query to a managed scraper API over httr2 and parsing the JSON. A direct rvest or httr2 request to a logged-out x.com URL returns a JavaScript shell with no tweet data, so those are the realistic no-account routes.
How do you turn scraped tweets into a data frame in R?
Parse the JSON response with httr2's resp_body_json(resp, simplifyVector = TRUE), which calls jsonlite under the hood, then pull the tweets array and pass it to tibble::as_tibble() or dplyr::bind_rows(). From there you select() the columns you want and write.csv() the result. The managed-API section below shows the full pattern end to end.
Is it legal to scrape Twitter (X) with R?
The language does not change the legal position. Scraping publicly visible X pages is generally treated as lawful in the United States, but X's Terms restrict automated access and set liquidated damages for large automated pulls, so review the Terms and your use case. I cover the CFAA and Terms detail in is scraping Twitter legal.