X Undocumented API
Overview
Use this skill for X.com's frontend web API: GraphQL operations under https://x.com/i/api/graphql, supporting REST-like web endpoints under https://x.com/i/api, and the browser headers/cookies those calls require.
First Rules
- Verify operation names and query IDs against the current X web bundle; query IDs rotate.
- Treat
operationNameas the durable conceptual endpoint, andqueryIdas rotating bundle metadata. - Never print raw cookies,
auth_token,ct0, bearer tokens, proxy credentials, or full upstream payloads. - Generate
x-client-transaction-idfrom the current X web page/bundle material and the exact request method/path. - Do not test mutations against live accounts unless the user explicitly asks for that mutation and understands the effect.
- Keep endpoint evidence with exact dates, operation IDs, variables, feature flags, response paths, account/access scope, and result counts.
- Separate upstream variables from local caller filters. A runtime or CLI
itemFiltercan be an application-side filter rather than a proven GraphQL variable.
Reference Map
Read only what the task needs:
- runtime-and-auth.md: browser auth, cookies, bearer tokens, CSRF, transaction IDs, headers, guest tokens, and error classes.
- graphql-discovery.md: extracting operation names, query IDs, feature flags, field toggles, and endpoint inventories from X web bundles.
- endpoint-patterns.md: request/response shapes for SearchTimeline, user/list/follower/tweet timelines, timeline instructions, and search operators.
- community-timelines.md: X Communities operations, variables, ranking modes, response paths, and community-search caveats.
- cursor-behavior.md: X timeline cursor semantics, pagination risks, fan-out caveats, and evidence standards.
- reply-visibility-research.md: high-fidelity notes from the 2026-05 reply visibility investigation, including observed failure modes, experiments, current best thinking, and next discriminators.
Reusable script:
scripts/extract-x-graphql-endpoints.ts: fetch current X web bundles and save a JSON/CSV operation inventory.
Investigation Workflow
- Clarify whether the user needs an API explanation, a current endpoint inventory, a request shape, or a live read-only probe.
- Extract or inspect the current X web bundle before relying on query IDs, variables, features, or response paths.
- For unknown GraphQL operations, locate nearby bundle code for variable names, feature flags, field toggles, and response normalization paths.
- For author-corpus work, compare native timeline operations against
SearchTimelineauthor queries and record lane yield per page, not just total pages. - For live read probes, use secret-safe output: status code, top-level keys, response path presence, item counts, cursor presence, and example public IDs only.
- For pagination behavior, record request cursor, returned cursors, item IDs in order, item count, first/last sort index, endpoint, variables, and observed time.
- Save dated findings outside the skill unless the user explicitly asks to update this skill.
GraphQL Request Shape
X web GraphQL paths have this form:
https://x.com/i/api/graphql/<queryId>/<operationName>?variables=<json>&features=<json>
Some operations also use fieldToggles=<json>.
Authenticated reads commonly need these headers:
authorization: Bearer <X web bearer token>
x-csrf-token: <ct0>
cookie: ct0=<ct0>; auth_token=<auth_token>
x-client-transaction-id: <generated for method + full path>
x-twitter-active-user: yes
x-twitter-auth-type: OAuth2Session
x-twitter-client-language: en
origin: https://x.com
referer: https://x.com/
The transaction ID path must include the query ID:
/i/api/graphql/<queryId>/<operationName>
Diagnostics
| Symptom | Likely cause | Next check |
|---|---|---|
| HTTP 401 or 403 | Expired cookies, missing ct0, bad bearer token, access restriction, account challenge |
Refresh browser session material; do not print secrets |
| Empty HTTP 404 | Missing/invalid x-client-transaction-id or stale query ID |
Verify full path includes query ID and regenerate transaction ID from current X material |
| HTTP 429 | Rate limited account, IP, or operation | Inspect X rate-limit headers and retry windows |
| HTTP 400 | Variable, feature, or field-toggle mismatch | Compare request payload with current bundle caller code |
| HTTP 200 with empty timeline | Access limitation, wrong response path, stale variables, logged-out limitation, or empty source | Inspect top-level keys and instruction path without dumping payload |
| Data is stale or incomplete | Search index behavior or ranking product choice | Prefer native timeline operations when available |
| Mutation returns HTTP 200 but no send captured in the HTTP log, yet the action took effect | The action rode a WebSocket, not an HTTP mutation (X Chat sends are WS frames to wss://chat-ws.x.com/ws) |
Use a WebSocket frame capture (CDP WebSocketFrameSent/Received), not request logging. HTTP GraphQL capture will never see these sends |
| Reply verification mismatch after REST or fallback send | X may prepend the leading @handle; the visible draft text is often the display_text_range slice |
Compare against legacy.full_text.slice(display_text_range) when available |
Current High-Value Facts
SearchTimelineusesrawQuery,product,count, optionalcursor, and promoted-content controls.SearchTimelineauthor queries can outperform native user timelines for corpus collection. In live xpool probes on 2026-06-18,from:<handle> -filter:replies -filter:retweetsandfrom:<handle> filter:repliesreached 100 lane items in 5 pages for sampled accounts where native timeline crawls hit only 39-47 items after 20 pages.- Search author-query cursors appear risky across account rotation. In live xpool profile-sample probes, later-page
SearchTimelinecontinuations under rotating accounts produced concentrated 404 / timeout / network failures; pinning one account per lane materially reduced that failure mix, though it did not remove empty-page degeneration. - Native list recency is
ListLatestTweetsTimeline, notSearchTimeline list:<id>. - Native community posts are
CommunityTweetsTimeline, not aSearchTimelinecommunity operator. - Direct messages are now "X Chat": end-to-end-encrypted, PIN-gated, and sent over WebSocket (
wss://chat-ws.x.com/ws?token=<JWT>), not a GraphQL/REST mutation. Legacy DM mutations (useSendMessageMutation, RESTdm/new2.json) are gone from current bundles. See the 2026-06-18 notes below. UserTweetsAndReplieslocalitemFilterhandling is not proof of an upstream GraphQL variable. In the 2026-06-18 bundle scan,itemFilter,authored_posts, andauthored_repliesstrings were absent from the current client-web bundle.UserTweetscan include repost surfaces; do not treat it as pure authored root-post history without checking surface kind or local filters. In 2026-06-18 live xpool probes, nativeUserTweetsrecovered materially more root-post items than author-search for some post-light accounts (64vs20,100vs74,22vs2in early comparisons, later22vs2,1vs1, and18vs0on three more post-light accounts), so a post lane may need native-timeline preference even when reply lanes stay on search.- Low-yield native post
cursor_loopis often a corpus-shape signal, not a pure transport failure. In one fresh xpool worker slice, low-yield post loops split into two buckets: sparse accounts where replies also stopped early (average lifetime tweet count about200) and reply-heavy accounts where replies still reached target (average lifetime tweet count about4300while root-post yield stayed near7). - In a larger hybrid-worker slice after the posts=
UserTweets/ replies=SearchTimelinerollout (1,534 samples since the 2026-06-18T21:39:50Z boot), reply lanes were dominated bytarget_items_reachedandsource_exhausted, while posts still split acrosstarget_items_reached,max_pages_reached, andcursor_loop. Treat that as evidence for a hybrid endpoint choice, not a single universally best authored-corpus endpoint. - A short
runner_terminatedburst immediately after a PM2 restart can be rollover noise from in-flight work rather than a steady-state cursor signal. In the same 2026-06-18 investigation, all freshrunner_terminatedreply-lane rows landed within about one second, 44-45 seconds after restart, with none afterward. TimelineTimelineCursorentries commonly carryTopandBottomcursor values.countis not always honored by timeline operations; observe returned item counts. Current live author-search probes emitted about 20 items/page even when higher counts were requested.
2026-06-18 Direct Messages / X Chat E2E Notes
These notes came from a live investigation (logged-in managed sessions via a CDP-backed browser tool, with WebSocket frame capture). Treat them as the current DM reality, superseding any older "send a DM via GraphQL" assumption.
- Legacy DM mutations are gone.
useSendMessageMutation(query idMaxK2PKX1F9Z-9SwqwavTwin the 2023-eratwitter-api-clientregistry) and the REST fallbackdm/new2.jsonare absent from every current X web bundle chunk. Do not reuse them; they will not work. - X has replaced DMs with "X Chat": end-to-end-encrypted and PIN-gated. Rolled out from Nov 2025 ("X Chat replacing DMs for some X users"). Authenticated
/messagesredirects to/i/chat/pin/newuntil the account is provisioned. - Send is a WebSocket frame, not an HTTP mutation. The send rides
wss://chat-ws.x.com/ws?token=<JWT>. HTTP GraphQL/REST capture during a real send observed zero send calls. There is no sendoperationNameto extract — bundle extraction can never surface a DM send query id, by design, not by capture gap. - PIN derives encryption keys client-side. Provisioning the passcode generated no server request carrying the PIN; keys are produced locally from it. A fresh browser context (no local key material) is redirected to
/i/chat/pin/recoveryand must re-enter the PIN to re-derive keys before any read or send. Cookie-only / headless sessions are therefore insufficient for X Chat without PIN re-entry. - DM-adjacent HTTP mutations that DO remain in current bundles:
dmBlockUser(IYw9u1KEhrS-t-BXsau4Uw),dmUnblockUser(Krbs6Nak_o7liWQwfV1jOQ),DmNsfwMediaFilterUpdate(of_N6O33zfyD4qsFJMYFxA),ConversationControlChange(57WYJNnWH0vM3Ip_gm8B2g),ConversationControlDelete(OoMO_aSZ1ZXjegeamF9QmA). These are block/unblock/control, not send. - Capture tooling caveats: CDP
Network.enabledoes not capture POST bodies unlessmax_post_data_sizeis set; and WebSocket frames need dedicatedWebSocketCreated/WebSocketFrameSent/WebSocketFrameReceivedhandlers — request handlers never see them. A CDP tool without WS handlers is blind to X Chat. - Reachability: accounts that have never followed / accepted a message request from each other cannot exchange DMs — recipient search at
/i/chatcompose returns only the sender, and a typed message becomes a self-chat ("You: ") the other account never receives.
2026-05-20 Reply Publish Visibility Notes
These notes came from a live, user-approved xpool reply probe plus current X web bundle inspection. Treat query IDs as dated facts and regenerate before reusing.
- Current observed
CreateTweetquery ID was5CdvsV_zjv4L64XFifAglw; current observedTweetDetailquery ID wasoCon7R-cgWRFy6EfZjaKfg. - A patched direct GraphQL reply send used
POST /i/api/graphql/5CdvsV_zjv4L64XFifAglw/CreateTweet, returned HTTP 200, had no top-level GraphQL errors, and returneddata.create_tweet.tweet_results. - Useful mutation evidence, without secrets: method, path, operation name, query ID, transaction-id state, HTTP status, content type, GraphQL error presence, response path,
ct0rotation boolean, rate-limit headers, body key names, created public tweet ID, parent tweet ID, and verifier result. - For replies, native-like variables include
tweet_text,media,semantic_annotation_ids, andreply.in_reply_to_tweet_idwithexclude_reply_user_ids. - Current web metadata includes
fieldTogglesonCreateTweet; include the current toggles when the bundle advertises them. - Native
engagement_requestis conditional, not universal. X web emitted it only when composer/source state had promoted/impression content with bothdisclosure_typeandimpression_id; missing it is a source-context gap for impression-backed targets, not proof every direct reply is non-native. TweetDetailparent-thread visibility can lag or paginate. In the live probe, the created reply was initially behind aShowMoreThreadscursor inrankingMode=Relevance, then appeared on page 1 a few minutes later.SearchTimelinecan be a false negative for newly created replies. In the live probe, the reply was visible through parentTweetDetail Relevance, whileSearchTimelinemissed it by bothconversation_id:<parent_id>and exact-text search.- Do not use
SearchTimelinepresence as the source of truth for reply visibility. It is a discoverability/indexing signal only. - First-party hidden replies are a separate surface. Check
ModeratedTimelinefor the parent before labeling a reply hidden; in the live probe it returned a valid empty hidden-replies timeline while parentTweetDetailshowed the reply. - Visibility verification should be a vector, not one boolean: direct lookup visible, author replies visible with cursor depth, parent
TweetDetailseen with ranking mode/page/cursor, search discoverable,ModeratedTimelinepresent/absent, policy wrapper/tombstone/limited-action state. - Later native-browser, real-Google-Chrome, and graph-connected xpool experiments are summarized in reply-visibility-research.md. The short version: accepted replies can be direct-visible to other viewers while absent from those viewers' parent
TweetDetail; a one-way poster-follows-parent-author edge did not clear that threshold in the sampled runs.