How to Scrape Twitter (X) With Node.js (2026)
- Node's browser routes work. Drive Chrome with Puppeteer or Playwright and read the GraphQL JSON X loads in the background. A logged-out
x.comprofile returns about 607 KB of HTML with zero tweet elements, so parsing the page itself gets you nothing. - Plain
fetchis the fragile route. Node 18+ can hit X's guest endpoints with no browser, butguest/activate.jsonreturned HTTP 403 for me in 2026, and the GraphQLdoc_idhashes rotate every few weeks. - The npm libraries changed. @the-convocation/twitter-scraper now needs a logged-in account for most methods, and Rettiwt-API's full mode is an API key built from your own X cookies.
- For a stable feed I call a managed API. One
fetchto ChocoData returns parsed JSON for a profile, tweets, or search, so the token refresh,doc_idchase, and proxies happen server-side.
The real question behind how to scrape Twitter with Node.js in 2026 is not “which library do I install,” it is “which route survives more than a few weeks.” I have built X data pipelines in Node, and the language was never the hard part. X (formerly Twitter) serves its timeline and search through a JavaScript front end backed by an internal GraphQL API, and that API is guarded by short-lived guest tokens and rotating doc_id query identifiers. Node hits that same wall as every other language.
This guide walks the Node.js routes that still return tweet data: Puppeteer, Playwright, plain fetch, the surviving npm libraries, and a managed API. Everything below is code I ran against live X targets in July 2026, with the exact point where each approach breaks.
Can you still scrape Twitter (X) with Node.js in 2026?
Yes, you can still scrape Twitter (X) with Node.js in 2026, but the page HTML no longer contains the tweets, so a plain request returns a shell you cannot parse. X moved its data behind an internal GraphQL API in 2023, and Node reaches it the same way the browser does: run the scripts, or replay the authenticated calls yourself.
Here is the wall in runnable Node. Save it as check.mjs so top-level await works, then run node check.mjs:
// Node 18+ ships a global fetch, no dependency needed.
const ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/124.0 Safari/537.36";
const res = await fetch("https://x.com/nasa", { headers: { "User-Agent": ua } });
const html = await res.text();
console.log(res.status); // -> 200
console.log(html.length); // -> ~607000
console.log((html.match(/data-testid="tweet"/g) || []).length); // -> 0
The status is 200 and the body is large, which is exactly what fools people coming from 2021 tutorials. Then the parser finds nothing, because the timeline was never in that response. Everything below is about reaching the data the page fetches after it loads, starting with a map of the Node routes that still work.
What are the Node.js methods to scrape Twitter?
There are four Node.js methods to scrape Twitter that still return data in 2026, and they trade off speed, fragility, and how much of the blocking lands on you. The durable pattern is the one every language converges on: drive a real browser and read the GraphQL JSON, or hand the whole problem to a managed API.
| Method | Node tooling | Auth needed | Output you read | Holds up under change |
|---|---|---|---|---|
| Headless browser + network capture | Puppeteer, Playwright | Session cookies or guest token | GraphQL JSON | Good, the JSON shape is stable-ish |
Direct GraphQL over fetch | Global fetch, undici | Guest token + current doc_id | GraphQL JSON | Weak, doc_id rotates |
| Reverse-engineered npm library | @the-convocation, Rettiwt-API | Logged-in account cookies | Parsed objects | Weak-to-fair, breaks on X changes |
| Managed scraper API | Any HTTP client | Provider key | Parsed JSON | Strong, provider absorbs changes |
The table’s lesson is that anything depending on a fixed doc_id or anonymous access is living on borrowed time, which is the recurring theme in the sections below. The browser routes are the sturdiest do-it-yourself option, so I will start there with Puppeteer.
How do you scrape Twitter with Puppeteer in Node.js?
You scrape Twitter with Puppeteer in Node.js by launching headless Chrome, opening the target X URL, and listening on the response event for X’s GraphQL calls, then parsing the JSON they return. Puppeteer’s interception model is documented in the Puppeteer network guide, and capturing the response beats scraping the DOM because the JSON already carries every field structured.
// npm i puppeteer
const puppeteer = require("puppeteer");
(async () => {
const browser = await puppeteer.launch({ headless: "new" });
const page = await browser.newPage();
const captured = [];
page.on("response", async (response) => {
const url = response.url();
if (url.includes("UserTweets") || url.includes("UserByScreenName")) {
try {
captured.push(await response.json());
} catch (e) {
/* non-JSON response, ignore */
}
}
});
await page.goto("https://x.com/nasa", { waitUntil: "networkidle2" });
await new Promise((r) => setTimeout(r, 4000));
console.log(`captured ${captured.length} GraphQL payloads`);
await browser.close();
})();
UserTweets and UserByScreenName are X’s own GraphQL operations, and their payloads hold the full tweet objects: text, created_at, public metrics, and author fields. The catch is that a default Puppeteer session carries a detectable automation fingerprint, so on a clean machine X often shows a login or challenge wall before the timeline loads. Stealth plugins and residential proxies cut the challenge rate, but they do not stop X from renaming an operation or rotating its doc_id. Playwright captures the same JSON with a slightly different API, which is the next route.
How do you scrape Twitter with Playwright in Node.js?
You scrape Twitter with Playwright in Node.js the same way as Puppeteer, opening the page and catching the GraphQL responses, but Playwright’s response event and built-in proxy support make the capture a little cleaner. The Playwright network guide documents the event, and the proxy launch option matters because X rate-limits hard per IP.
// npm i playwright && npx playwright install chromium
const { chromium } = require("playwright");
(async () => {
const captured = [];
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
page.on("response", async (response) => {
const url = response.url();
if (url.includes("UserTweets") || url.includes("UserByScreenName")) {
try {
captured.push(await response.json());
} catch (e) {
/* not JSON, skip */
}
}
});
await page.goto("https://x.com/nasa", { waitUntil: "networkidle" });
await page.waitForTimeout(4000);
console.log(`captured ${captured.length} GraphQL payloads`);
await browser.close();
})();
The win is identical to Puppeteer: you read UserTweets JSON instead of brittle CSS selectors that break whenever X reskins its interface. To page past the first batch, scroll with page.mouse.wheel(0, 3000) in a short loop and let the same response hook keep firing as new UserTweets calls load. When a capture suddenly returns empty, the cause is almost always that X renamed the operation or rotated its doc_id, not a bug in your code. If you would rather skip Chrome entirely, you can issue the GraphQL requests straight from Node, which is lighter and far more fragile.
Can you scrape Twitter with plain Node.js and fetch (no browser)?
You can scrape Twitter with plain Node.js and fetch by acquiring a guest token, then calling X’s GraphQL endpoints with that token and the current doc_id. It is lighter than Puppeteer because there is no Chrome process, and it is the most fragile route because you own the token refresh and the doc_id values yourself.
The flow starts by posting X’s public web bearer to the guest activation endpoint to get an x-guest-token. That first step is already where it stalls:
// Node 18+ has a global fetch. Save as guest.mjs
const BEARER = "Bearer <PUBLIC_WEB_BEARER_TOKEN>"; // X's public web token, rotates
const res = await fetch("https://api.x.com/1.1/guest/activate.json", {
method: "POST",
headers: { Authorization: BEARER },
});
console.log(res.status); // -> 403 for me in 2026
const data = await res.json();
console.log(data.guest_token); // undefined when the call is refused
When I hit guest/activate.json with a browser User-Agent in 2026, it returned HTTP 403 instead of a token, which is enough to stall any guest-token client until its maintainer ships a workaround. Even when activation succeeds, guest tokens are short-lived and IP-bound, so a real job re-activates them constantly and handles the Rate limit exceeded JSON X returns past its quiet per-token ceiling. X’s own Developer Policy prohibits scraping, browser automation, and unofficial access methods outright, and the web app enforces parts of that with the bot detection you are fighting here. The npm packages that wrapped this flow tried to hide the pain, so they are worth a look next.
What Node.js libraries still scrape Twitter (X)?
Two Node.js libraries still scrape Twitter (X) in 2026, and both work by replaying X’s internal frontend API with a real account’s session cookies rather than the official API. The no-token era that made them popular is over, so each now expects authentication for anything beyond a trivial read.
@the-convocation/twitter-scraper is the maintained Node port of the archived n0madic/twitter-scraper. Its README is blunt that several operations now require logging in with a real account through scraper.login(), that cookie auth exported from an existing browser session is the recommended path, and that any account you log in with “is subject to being banned at any time.” It keeps an ergonomic API for tweets, profiles, and search, but the auth requirement and the ban risk are yours to manage.
Rettiwt-API splits access into two modes. Its guest mode needs no login and covers tweet details, user details by username, and a user timeline. Its full mode uses an API key that is just your own auth_token, ct0, and twid cookies base64-encoded, which the README warns carries the same authorization as your logged-in account. Neither library needs an official X API key, and both break on the same schedule X changes its backend, which is the pattern worth understanding before you build on either one.
Why do Node.js Twitter scrapers keep breaking?
Node.js Twitter scrapers keep breaking because X changes the pieces your code depends on every few weeks: it rotates guest tokens, changes the GraphQL doc_id query identifiers, and shifts per-IP rate limits. None of these are documented for scrapers, so you find out when your job starts returning empty payloads or Rate limit exceeded errors.
The libraries themselves are the evidence. The most popular Go scraper was archived in late 2023 with a note that all its methods now require authentication, and the Node port inherited that reality. The two maintained npm libraries above moved to cookie auth for the same reason: single-session, anonymous scraping stopped being reliable.
The official escape hatch is not cheap. X’s Developer Platform runs on pay-per-use pricing, and the X API pricing docs list post reads at $0.005 per resource and reads of your own data at $0.001 per resource, deduplicated within a 24-hour window. The standalone free read tier is gone, so a Node job that reads even a few hundred thousand posts a month runs into real per-resource billing. There is also a legal layer: US courts have treated scraping public, logged-out data favorably, as in hiQ v. LinkedIn, but X’s terms restrict automated access and large-volume collection, so I keep the block-avoidance detail in a separate guide on scraping X without getting blocked. That maintenance-and-cost math is the reason the last route exists.
How do you scrape Twitter with Node.js without managing tokens or proxies?
You scrape Twitter with Node.js without managing tokens or proxies by sending the target to a managed scraper API that returns parsed JSON, while the provider handles the guest tokens, doc_id rotation, and proxy pool. You send one authenticated request from Node and get structured data back, with no Chrome process and nothing to patch when X changes.
The request is a plain GET with your API key as a query parameter. This is the shape I use, shown first as curl:
curl "https://chocodata.com/api/v1/twitter/profile?username=nasa&api_key=$CHOCO_API_KEY"
The Node version is the same call through global fetch, dropping straight into an object you can use:
// Node 18+ global fetch. Set CHOCO_API_KEY in your environment.
const qs = new URLSearchParams({ username: "nasa", api_key: process.env.CHOCO_API_KEY });
const res = await fetch(`https://chocodata.com/api/v1/twitter/profile?${qs}`);
const { data } = await res.json();
console.log(data.name, "-", data.followers_count, "followers");
When I probed the API in 2026, requests authenticated against api.chocodata.com and an invalid key was rejected before it returned data, which matches the documented rule that unauthorized requests do not consume credits. To collect a user’s tweets or a search instead, you change the resource and read the list the same way:
// Pull a user's recent tweets as an array.
const qs = new URLSearchParams({ username: "nasa", count: "50", api_key: process.env.CHOCO_API_KEY });
const res = await fetch(`https://chocodata.com/api/v1/twitter/tweets?${qs}`);
const { data } = await res.json();
console.log(`${data.tweets.length} tweets`);
console.log(data.tweets[0].created_at, "-", data.tweets[0].favorite_count);
Each X object maps to its own endpoint on the same base and key:
| You want | Endpoint | Example query |
|---|---|---|
| Profile / account | /twitter/profile | ?username=nasa |
| Tweets / posts | /twitter/tweets | ?username=nasa&count=50 |
| A single tweet | /twitter/tweet | ?id=<tweet_id> |
| Search / hashtag / trends | /twitter/search | ?query=<terms> |
| Followers / following | /twitter/followers | ?username=nasa |
| Images / video | /twitter/media | ?username=nasa |
You can get an API key from ChocoData and swap it into the snippets above. For a one-off pull of a few hundred records, a Puppeteer script in Node is fine. For a feed you need to keep alive, moving the token refresh and doc_id chase to a provider is the cheaper trade once you price in the patching.
Which Node.js method should you choose to scrape Twitter?
The right Node.js method to scrape Twitter depends on volume, on whether you can risk an X account, and on how much engineering time you want to spend chasing X’s changes. Here is the summary I give people who ask.
| If you need… | Use | Why |
|---|---|---|
| A quick research grab, can run a browser | Puppeteer or Playwright + GraphQL capture | No account needed, but slow and challenge-prone |
| Ergonomic calls, can risk an account | @the-convocation or Rettiwt-API with cookies | Replays the frontend API, ties the run to a suspendable account |
| The lightest DIY route, low volume | Plain fetch + guest token | No Chrome, but you chase doc_id and tokens yourself |
| Thousands of records, no tokens or proxies | Managed scraper API (ChocoData) | Proxies, tokens, and parsing are server-side, one key, parsed JSON |
| The fully sanctioned route, budget for it | Official X API (pay-per-use) | Compliant access, but metered per post read |
For a handful of accounts, a browser script in Node is genuinely fine and free. It stops being free the moment you price in the hours spent re-finding doc_id values and rotating proxies, which is the trade every route in this guide points toward. If you want the single-language deep dive, I keep a complete Python walkthrough with the cookie-auth libraries, and I rank the managed options head to head in my best X scrapers in 2026 roundup.
FAQ
Is there an official Twitter (X) API for Node.js?
Yes. The official X API v2 is the sanctioned route, and you call it from Node with fetch and a bearer token like any REST API. It runs on pay-per-use pricing now, where post reads are metered per resource and the same resource requested twice in 24 hours is charged once, so the bill is predictable but climbs fast for collection work. Most hobby and mid-size Node projects get priced off it, which is why the browser and managed routes still exist.
Does Puppeteer get blocked when scraping X?
A default Puppeteer or headless-Chrome session is detectable through its automation fingerprint and gets challenged on X before the timeline loads. People cut the challenge rate with stealth plugins, residential proxies, and human-like pacing, and by capturing the GraphQL JSON instead of scrolling and parsing the DOM. Even then X rotates its doc_id query identifiers periodically, so a working Puppeteer scraper still needs maintenance.
How do I avoid the doc_id and guest-token breakage in Node?
Use a managed scraper API that tracks X's changes for you. You send a username, tweet ID, or search query and get parsed JSON back, which moves the guest-token refresh, doc_id rotation, and proxy rotation to the provider's side. I show the Node fetch shape in the managed-API section of this guide.
Can I scrape Twitter (X) with TypeScript?
Yes. Every Node route here works in TypeScript, and both maintained libraries ship their own types: @the-convocation/twitter-scraper and Rettiwt-API are written in TypeScript. The managed fetch call is identical, you just type the JSON response. The blocking mechanics do not change with the language, so the choice comes down to your existing stack.
Is it legal to scrape Twitter (X) with Node.js?
The language does not change the legal picture. Scraping publicly visible, logged-out X data has been treated favorably by US courts, but X's Terms of Service restrict automated access without permission and set liquidated damages for high-volume collection, so the safer path for a business feed is usually a provider operating within its own agreements. Logged-in scraping with borrowed cookies sits in a different, riskier position than logged-out collection.