Twitter API Integration (RSSHub)#
Overview#
RSSHub's Twitter integration lives under lib/routes/twitter/ and converts X (Twitter) timelines, lists, searches, and conversations into RSS feeds. All active routes use the web-api path, making GraphQL requests to https://x.com/i/api . The mobile API was removed in October 2025 after Twitter introduced client attestation ; the developer (Pay-Per-Use) API remains available for user and keyword routes.
Required configuration (at least one auth method):
| Env var | Purpose |
|---|---|
TWITTER_AUTH_TOKEN | Comma-separated auth_token cookies from logged-in Twitter Web β recommended path |
TWITTER_CONSUMER_KEY / TWITTER_CONSUMER_SECRET | Developer API credentials |
TWITTER_ACCESS_TOKEN / TWITTER_ACCESS_SECRET | Optional user-auth tokens for developer API |
TWITTER_THIRD_PARTY_API | Optional proxy endpoint for a subset of operations |
Route Map#
| Route | Handler | Notes |
|---|---|---|
/twitter/user/:id/:routeParams? | user.ts | includeReplies switches between UserTweets and UserTweetsAndReplies GQL operations |
/twitter/list/:id/:routeParams? | list.ts | Uses ListLatestTweetsTimeline; id is the list ID |
/twitter/home / /twitter/home_latest | web-api | HomeTimeline / HomeLatestTimeline GQL operations |
/twitter/keyword/:keyword | web-api | SearchTimeline |
/twitter/tweet/:id | web-api | TweetDetail with conversation expansion |
The id parameter for user routes accepts either a screen name or a unique numeric ID prefixed with + (e.g., +44196397) .
Universal routeParams options (query-string format) include display controls (readable, authorNameBold, showAuthorInTitle, etc.) and feed-shaping flags :
includeReplies/includeRtsβ only for/twitter/userforceWebApiβ forces web-api even when a developer API key is configured, available for/twitter/userand/twitter/keywordcountβ passed directly to the Twitter API
Authentication & Request Flow#
All web-api calls go through twitterGot in utils.ts, which uses undici.fetch directly β not ofetch or the global request rewriter β to preserve the CookieAgent dispatcher. This was a critical production fix: the standard Request constructor silently drops the dispatcher option, causing cookies to never be sent .
Token selection and cookie hydration:
getAuthround-robins across theTWITTER_AUTH_TOKENlist. Each token is locked under the cache keytwitter:lock-token1:<token>(TTL 20 s) to prevent concurrent reuse.token2Cookiewraps the token in aCookieJar, fetcheshttps://x.comto hydrate session cookies, and caches the result undertwitter:cookie:<token>.
For third-party API mode (TWITTER_THIRD_PARTY_API), requests are routed through ofetch to the proxy endpoint instead of twitterGot. This mode only covers the operations listed in thirdPartySupportedAPI : UserByScreenName, UserByRestId, UserTweets, UserTweetsAndReplies, ListLatestTweetsTimeline, SearchTimeline, UserMedia.
GraphQL Query ID Resolution#
Twitter rotates GraphQL query IDs every 2β4 weeks, breaking all routes with 404 errors. gql-id-resolver.ts (introduced in PR #21544) resolves this dynamically:
- Fetches
https://x.comHTML and extracts theclient-web/main.<hash>.jsbundle URL . - Downloads the bundle and regex-extracts all
queryId/operationNamepairs . - Caches the result under
twitter:gql-query-idsusingconfig.cache.contentExpire. - Falls back to hardcoded IDs if extraction fails .
A shared resolvePromise deduplicates concurrent resolution attempts during startup . The resolved map is built via buildGqlMap(), which produces paths like /graphql/<id>/<OperationName> .
constants.ts initializes gqlMap from fallbackIds at module load and rebuilds it via initGqlMap(), which is wired as api.init() and called before the first API request .
Covered operations and their fallback IDs :
| Operation | Fallback ID |
|---|---|
UserTweets | E3opETHurmVJflFsUBVuUQ |
UserTweetsAndReplies | bt4TKuFz4T7Ckk-VvQVSow |
HomeTimeline | xhYBF94fPSp8ey64FfYXiA |
HomeLatestTimeline | 0vp2Au9doTKsbn2vIk48Dg |
TweetDetail | QuBlQ6SxNAQCt6-kBiCXCQ |
SearchTimeline | UN1i3zUiCWa-6r-Uaho4fw |
ListLatestTweetsTimeline | Pa45JvqZuKcW1plybfgBlQ |
Cache Key Isolation#
Bug (issue #22864): getUserTweets, getUserTweetsAndReplies, and getUserMedia all passed anonymous arrow functions as callbacks to cacheTryGet. Because Function.name is "" for anonymous functions, all three operations produced the same cache key β twitter:<rest_id>::{"count":17} β causing one operation's results to be served for another.
Fix (commit 6a265e9d8): cacheTryGet now accepts an explicit operationName string as its third argument:
cache.tryGet(getTwitterUserCacheKey(id, operationName, params), ...)
Each operation passes its own string literal :
| Function | operationName |
|---|---|
getUserTweets | 'getUserTweets' |
getUserTweetsAndReplies | 'getUserTweetsAndReplies' |
getUserMedia | 'getUserMedia' |
getUserLikes | 'getUserLikes' |
getUserTweet | 'getUserTweet' |
User profile data is cached separately under twitter-userdata-${id} , independent of timeline operations.
Timeline & Conversation Parsing#
All timeline data flows through gatherLegacyFromData(entries, filterNested?, userId?) in web-api/utils.ts. It filters the raw GraphQL instruction entries in two passes:
- Direct tweets: entries with
entryIdstarting withtweet-orprofile-grid-0-tweet-are included as-is . - Nested conversation modules: if
filterNestedis provided, entries whoseentryIdmatches any prefix have theirentry.content.itemsexpanded and flattened into the result .
Per-operation filterNested prefixes :
| Operation / function | filterNested | userId filter |
|---|---|---|
getUserTweetsAndReplies | ['profile-conversation-'] | β own tweets only |
getUserTweet (TweetDetail) | ['homeConversation-', 'conversationthread-'] | β |
getList | ['listConversation-'] | β |
Known bug and fix (issue #19788): The list timeline parser was dropping tweets contained in listConversation-* modules β including an author's own follow-up posts. The fix (commit b879d8d7f) passes ['listConversation-'] as filterNested to gatherLegacyFromData so those modules are expanded rather than skipped.
Special content types handled inside gatherLegacyFromData:
- Long-form notes (
note_tweet):full_textis replaced with the note body and entities are re-extracted . - Subscriber-only previews (
TweetPreviewDisplay): a syntheticlegacyobject is constructed with a[Subscribers Only]prefix so the entry still appears in the feed .