Open-source project
prinsss/twitter-web-exporter avatar
prinsss/twitter-web-exporter

Twitter Web Exporter reads your own GraphQL responses, and it has no test script

Export tweets, bookmarks, lists and much more from Twitter(X) web app.

2,744 stars210 forksTypeScriptMIT

At a glance

What is it?
A userscript that installs a network interceptor in the page, harvests the responses your own browser already receives, and writes them out as JSON, CSV or HTML. It needs no API key and sends nothing to a cloud, and it is bounded by exactly what Twitter has already rendered for you.
Who is it for?
Read the platform terms before you run anything here. The project argues the behaviour is close to copying data by hand and that no automatic botting is involved, which is a claim about the mechanism and not a permission, and the interceptor design means the script is bound to the private, undocumented internals of a web app that can change under you at any time.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 42 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The data comes from a network interceptor, not from the API

The mechanism is stated plainly in the FAQ. The script does not send any request to the Twitter API. It installs a network interceptor to capture the response of the GraphQL request initiated by the Twitter web app, then parses that response and extracts the data from it.

Every consequence follows from that one choice. No developer account and no API key are needed, because no request is ever issued. Nothing is uploaded, because everything is processed in the local browser and no data is sent to a cloud. And no API rate limit applies, because the script is not a client of the API.

The same design has a failure mode the project tracks separately. There is a dedicated issue for the case where the script captures nothing at all, filed under Content-Security-Policy issues, and the FAQ points readers there. Injecting an interceptor into another origin's page is precisely the situation a Content-Security-Policy governs, so a silent zero capture is an expected outcome rather than a mystery, and the FAQ treats it that way. The FAQ section ends with a question that begins asking about the difference from other tools and stops there.

It only sees what the page has loaded, which caps a profile at 3200 tweets

The limitation section states the governing rule: the script sees what you see on the page, and if the data is not visible to you it is not available to the script either.

Applied to profiles, that produces a hard number. Twitter displays only the latest 3200 tweets on a profile page, so nothing older than that can be exported, no matter how the request is made.

Applied to lists and timelines, it produces manual work. Page data is loaded dynamically, so the script cannot touch a record until the web app has fetched it, and the instruction is to keep scrolling until everything is loaded before exporting. For long lists the FAQ says you may need to scroll to the bottom of the page. The completeness complaint in the FAQ and the capture instructions in the usage section are the same problem described twice.

There is no automated alternative inside the project. Exporting is not automated and relies on human interaction to trigger the web app's own fetching. An issue collects community solutions using third party tools such as auto scrolling.

Direct messages are on the feature list and marked broken in the same breath

The feature list runs to eleven bullets: tweets, replies and likes of any user as JSON, CSV or HTML; bookmarks; following and followers lists; list members and subscribers; tweets from the home timeline and from list timelines; search results; bulk media download at original size; no developer account or API key; shipping as a userscript with everything done in the browser; data that never leaves the computer; and free and open source.

The sixth bullet reads: export direct messages, now broken, with a link to issue 116.

A capability listed alongside working features and simultaneously marked broken is the kind of thing a reader skims past. Anyone planning an archive that includes their own message history should assume that part is empty and check the issue before counting on it.

The capture surface is narrower than the feature list suggests. The script attaches itself automatically on five page types: a user profile, where it picks up tweets, replies, media and likes; the bookmarks page; the search results page; the following and followers page; and the list members or subscribers page. Anything outside those five is not in scope.

Media export zips into browser memory, and the ceiling is 2GB

Bulk media download is the feature with the most moving parts. Images and videos are downloaded to browser memory first, then zipped into a single archive, and a large enough set will crash the browser. The stated maximum depends on the browser and on available memory: 2GB on Chrome and 800MB on Firefox.

That number is a memory ceiling, not a feature limit, and it is the one hard boundary in the tool that a reader cannot work around by scrolling longer.

Two escape hatches exist. The Rate limit option throttles the download, and its default value of 1000 means the script waits one second after each file completes. And if you would rather not hold the archive in memory at all, the panel can copy the media URLs instead, leaving the fetching to an external download manager.

A separate switch governs the data export rather than the media. Checking Include all metadata pulls in every field the API response carries, which the text warns can significantly increase the size of the export.

The manifest has no test script, and tsc runs inside the build instead

The script list is short enough to take in at a glance:

code
"dev": "vite",
"build": "tsc && vite build",
"prepare": "husky",
"lint": "eslint .",
"changelog": "git-cliff -o CHANGELOG.md"

Type checking is not a separate step. It is the first half of the build command, so a type error fails the bundle and nothing else guards it. There is no test script and no test framework among the dependencies, in a codebase whose central job is parsing intercepted GraphQL responses whose shape is undocumented and can change without notice. The closest thing to verification is the lint pass and the type checker.

The dependency list also splits into two styles. All ten runtime dependencies are pinned to exact versions with no range, from @preact/signals at 2.0.0 through preact at 10.26.4, while every development dependency uses a caret range. For a userscript that ships as a single built file, exact runtime pins are the sensible half of that choice.

The lockfile is bun.lock while every script reads like an npm command

The repository root contains bun.lock and no npm or yarn lockfile, and the manifest declares no packageManager field. Meanwhile the scripts are written in the npm idiom, `npm run` style names throughout, and there is a .nvmrc pinning the development Node version.

So the lockfile format and the documented tooling disagree: the dependency graph is locked in Bun's format while the commands in the manifest read as npm scripts. Nothing in the README mentions which package manager to use, which for a project whose install step is a browser userscript rather than a registry install is a smaller gap than it would be for a library.

The rest of the root explains the rest of the build. vite-plugin-monkey produces the userscript, vite-plugin-i18next-loader handles translations, tailwind.config.js with daisyui and a set of postcss plugins covers styling, eslint.config.js with typescript-eslint and prettier covers linting, cliff.toml with git-cliff generates the changelog, and .commitlintrc with .husky enforce commit messages. docs/ holds the Simplified Chinese README alongside the English one.

The manifest is private, so distribution is one user.js asset from a release

The manifest sets `private` to true. The package is never published to a registry, which is consistent with the whole design: there is nothing to install from npm, because the artifact is a userscript.

Installation is two steps. Install Tampermonkey or Violentmonkey in the browser, then open a single file from the latest release, whose address ends in twitter-web-exporter.user.js. Nothing is compiled locally to use it.

Once it is running, the interface is a floating panel on the left of the page. A cat button closes the panel and reopens it, and the same panel can be reached from the userscript manager icon in the browser menu bar. A cog button opens settings for the UI theme and for enabling or disabling individual script features. The panel shows how many records have been captured, and an arrow button opens a data table where captured rows can be previewed and selected before export to JSON, CSV or HTML.

The tags tell their own story: v1.4.1 on 2026-07-29, v1.4.2 on 2026-08-19 and v1.4.3 on 2026-08-31, the last of them matching the final push to the default branch.

Editorial conclusion

Read the platform terms before you run anything here. The project argues the behaviour is close to copying data by hand and that no automatic botting is involved, which is a claim about the mechanism and not a permission, and the interceptor design means the script is bound to the private, undocumented internals of a web app that can change under you at any time. Then check four things: scroll a page to the bottom before exporting, because nothing lazy-loaded exists yet; expect direct messages to be missing, since that entry is marked broken; keep media sets small, because originals are zipped in browser memory with a stated ceiling of 2GB on Chrome and 800MB on Firefox; and treat the bookmark export as the reason to use it, since that is where it beats the official API's 800 item limit. Everything stays on your machine, which is the one claim here with no asterisks.

Frequently asked questions

Does Twitter Web Exporter need a developer account or an API key?

No. The project states the script sends no request to the Twitter API. It installs a network interceptor that captures the GraphQL responses the web app itself initiates, then parses them. Everything is processed in the local browser and no data is sent to a cloud.

How much of an account can Twitter Web Exporter actually capture?

Only what the page has already loaded, on five page types: a user profile, the bookmarks page, search results, the following and followers page, and the list members or subscribers page. Twitter displays only the latest 3200 tweets on a profile page, and because data is lazy-loaded you need to scroll to the bottom of long lists before exporting.

Can Twitter Web Exporter export bookmarks past the usual limit?

Yes, and that is one of the reasons it exists. The official API applies an 800 limit when accessing bookmarks, while the script exports all of them, stopping only when the Twitter web app itself imposes a limit.

Will my account be suspended for using Twitter Web Exporter?

The project's own answer is that suspension is not likely, because no automatic botting is involved and the behaviour is similar to manually copying data from the page. It also notes the web app has its own rate limit, and that trying again after a few minutes is the remedy when you hit it. That covers the mechanism, not the platform terms.

Why build Twitter Web Exporter when Twitter offers its own archive?

For archival use. The stated reasons are that Twitter's own archive contains only the numeric user ID of your following and followers, which is not human readable, and that the archive does not contain your bookmarks at all.

What is the size limit on Twitter Web Exporter media downloads?

Images and videos are downloaded into browser memory and then zipped into a single archive, so a large set of originals can crash the browser. The stated maximum is 2GB on Chrome and 800MB on Firefox. The Rate limit option defaults to 1000, which means waiting one second after each file, and you can copy media URLs instead to use an external download manager.

Official sources

  1. Issues
  2. License: MIT
  3. prinsss/twitter-web-exporter on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/prinsss-twitter-web-exporter.svg)](https://hysenlabs.com/projects/prinsss-twitter-web-exporter)