The Twitter (X) GraphQL API: What It Is, Why It Breaks, and What to Call Instead

This page is about the thing itself: what the "Twitter GraphQL API" is, how a request to it is built, why it keeps breaking, what X's terms say about it, and which published endpoint replaces each operation. It is not a guide to one library; our twscrape review covers that account-pool library and its own migration path, and our guide to scraping Twitter without the official API compares scraping routes in general. Here the subject is the private GraphQL layer that both of those sit on, read from the source code of two maintained open-source clients and from X's own terms page.
Developers usually meet it the same way: they open DevTools on x.com, see requests to /i/api/graphql/... returning exactly the JSON they want, and wonder whether they can call it too. Technically you can copy a request out of the Network tab. In practice the request is tied to a logged-in X account, to a query-ID hash that changes when X ships a new web build, to dozens of feature flags, and to a transaction header computed in the browser. None of it is documented, and X's terms forbid automated access outside its published interfaces.
So we did two things no other page on this keyword does. We read the full commit history of twscrape's api.py on GitHub to count how often the hard-coded GraphQL query IDs had to change. And we called the published equivalent of five common operations for 10 organization accounts (@XDevelopers, @github, @vercel, @OpenAI, @nvidia, @NASA, @Docker, @stripe, @Cloudflare, @SpaceX), measuring which of the fields twscrape parses from GraphQL come back and how long each call takes.
What the Twitter (X) GraphQL API actually is
X's web app is a client of X's own backend. When you open a profile on x.com, the page issues a GET to a URL of the form https://x.com/i/api/graphql/<queryId>/<OperationName>; twscrape's source names this base directly (GQL_URL = "https://x.com/i/api/graphql" in twscrape/api.py). The operation name says what the call does (UserByScreenName resolves a handle to a profile, UserTweets returns a profile's timeline, SearchTimeline runs a search, TweetDetail returns a post and its conversation). The query ID is a short hash identifying one compiled version of that operation.
GraphQL lets a client ask for exactly the fields one screen needs, which suits a fast-moving web app. What X has not done is publish that schema, document the operations, or offer keys for them. The public, documented products are the X API (the /2/... endpoints at docs.x.com) and enterprise data agreements. The GraphQL endpoints are an implementation detail of x.com that happens to be visible in a browser.
That is why the phrase "Twitter GraphQL API" mostly appears in two places: in the source code of open-source clients that replay those browser requests (twscrape describes itself as a library "for X/Twitter Search and GraphQL endpoints"; twikit keeps its operation list in twikit/client/gql.py), and in questions from developers who found the requests in DevTools. There is no X documentation page for it to rank, so searches for it land on blog posts, package pages and forum threads.
Operations we verified in source this session. twscrape's api.py on main (last changed in October 2026) defines 21 GraphQL operation constants, including UserByScreenName, UserByRestId, AboutAccountQuery, UserTweets, UserTweetsAndReplies, UserMedia, SearchTimeline, TweetDetail, Followers, Following, BlueVerifiedFollowers, Retweeters, ListLatestTweetsTimeline, ListMembers, CommunityQuery, CommunityTweetsTimeline, Bookmarks and GenericTimelineById (trends). twikit's gql.py adds write and per-ID operations such as TweetResultByRestId, TweetResultsByRestIds, CreateTweet, FavoriteTweet, CreateRetweet, Favoriters and HomeTimeline.
Anatomy of one GraphQL request: query ID, variables, features, tokens
A GraphQL call from the web client is a GET with two or three JSON-encoded query parameters and a set of headers. Reading twscrape's _gql_items method and its queue client, the pieces are these.
The query ID. The path segment before the operation name, for example OdBLACRZ0-9NikpJotaT7g in OdBLACRZ0-9NikpJotaT7g/UserTweets (twscrape main). It identifies a persisted query compiled into x.com's JavaScript bundles. When X ships a new build, the ID can change; per twscrape issue #306 (May 2026), an outdated hash makes the endpoint return "HTTP 404 with an empty body". The same operation has different IDs in different clients: twikit's gql.py, last changed in January 2025, still carries QWF3SzpHmykQHsQMixG0cg/UserTweets.
variables. The operation's arguments as JSON: a userId or screen_name, a count, a cursor for the next page, and for search a rawQuery and product (Latest, Top, People, Media).
features. A JSON object of boolean feature flags that tells the backend which fields and behaviors the caller supports. twscrape's GQL_FEATURES dictionary currently holds 39 of them, with a comment telling maintainers to look the values up in x.com's page source. If the server expects a flag the client does not send, the request fails; twscrape has a dedicated GqlFeaturesOutdatedError for that case.
Authorization. A bearer token belonging to the web client itself (twikit stores it as TOKEN in constants.py), plus either a guest token or a logged-in session. twikit's guest client calls an activate endpoint "generating a guest token" and sends it as X-Guest-Token; twscrape's README is blunt that it "requires authorized X/Twitter accounts" and recommends importing the auth_token and ct0 cookies from a browser. ct0 doubles as the CSRF token header.
x-client-transaction-id. Since 2026, per the same issue #306, "X now requires that every API call include the x-client-transaction-id header, generated client-side" from a JavaScript file x.com serves; without it the endpoint again answers 404. twscrape now ships a xclid.py module to compute it and retries with a fresh value when a 404 comes back.
Add those up and the "API" is really a moving protocol between one website and its own backend. Every one of those inputs is controlled by X and can change without notice, which is the point of the next section.
GET https://x.com/i/api/graphql/OdBLACRZ0-9NikpJotaT7g/UserTweets
?variables={"userId":"13334762","count":20,...}
&features={...39 boolean flags...}
authorization: Bearer <web-client token>
cookie: auth_token=<session>; ct0=<csrf>
x-csrf-token: <ct0>
x-client-transaction-id: <computed per request>
# Shape of the request as built by twscrape (api.py, queue_client.py, xclid.py).
# Shown for explanation only; the IDs and flags above go stale with each x.com build.How often the query IDs change: 40 versions of twscrape's api.py
"Query IDs rotate" is easy to say, so we measured it from public history. twscrape hard-codes every GraphQL operation as a constant such as OP_UserTweets = "<queryId>/UserTweets". We downloaded every version of twscrape/api.py committed since the file was created in mid-2024 (40 file versions from the GitHub commit history) and diffed the constants.
14 of those 40 commits rewrote at least one query ID: 2 in 2024, 3 in 2025 and 9 in 2026. A typical rewrite replaced almost everything at once: 16 of 16 IDs in each 2024 update, 13 to 19 IDs per update in 2026. That pattern fits X rebuilding its web bundles and the maintainer re-extracting the hashes, not individual operations being retired.
The 2026 cluster is visible in the chart: after a quiet stretch from May 2025, issue #306 (opened May 10, 2026) reported that every GraphQL call failed, for two reasons at once (stale IDs and the newly required transaction header). The fix landed on May 20, 2026, and the file then needed a fresh round of IDs every one to eight weeks through October. The maintainer also dropped calls when X stopped serving them: Favoriters and Likes in March 2025, and the user_by_id method in May 2026 (commit message: "drop user_by_id because X no longer supports it"; restored in August 2026).
What this cannot show: twscrape updates when its maintainer or contributors notice breakage, so the commit dates are a lower bound on how often X's IDs actually changed, not a precise schedule. Some IDs may have changed and changed back between commits without leaving a trace in this file.

What X's terms say about calling it
The legal side is not ambiguous. X's Terms of Service, fetched from x.com/en/tos (effective October 9, 2026), list among the things you may not do: "access or search or attempt to access or search the Services by any means (automated or otherwise) other than through our currently available, published interfaces that are provided by us (and only pursuant to the applicable terms and conditions), unless you have been specifically allowed to do so in a separate agreement with us". The same item adds: "crawling or scraping the Services in any form, for any purpose without our prior written consent is expressly prohibited".
The private GraphQL endpoints are not a published interface; they have no documentation, no developer terms and no keys. The published interface for programmatic access is the X API, governed by the developer agreement linked from docs.x.com.
Liquidated damages. The terms also state that a user who violates them, or induces or knowingly facilitates others to do so, is liable "for requesting, viewing, or accessing more than 1,000,000 posts (including reply posts, video posts, image posts, and any other posts) in any 24-hour period - $15,000 USD per 1,000,000 posts" (the EU, EFTA and UK version states EUR 15,000). That clause applies to the account holder whose session made the requests.
Account consequences. Because every logged-in GraphQL call runs as a specific X account, that account is what X rate-limits, locks or suspends. twscrape's own README warns that "X/Twitter's Terms of Service discourage using multiple accounts", and its issue tracker is full of locked-account and 403 reports, such as issue #337 (September 2026), where user_by_id calls started returning 403 for an account whose cookies otherwise worked.
This page does not recommend calling the private GraphQL API. If you maintain code that does, the rest of the page is about moving it to an interface that is published and priced.
The mapping table: GraphQL operation to published endpoint
Every read operation the two libraries use has a documented counterpart on twitterapi.io, a REST API with fixed paths on https://api.twitterapi.io (such as /twitter/user/info), one X-API-Key header, and no X account needed for reads. Operation names come from twscrape api.py and twikit gql.py on GitHub; endpoint paths come from our OpenAPI spec.
Three practical differences matter when you port code. First, pagination is plain: every list endpoint returns has_next_page and next_cursor, and you pass cursor back; there is no tree of timeline entries to walk for a Bottom cursor. Second, the response is already flat: a tweet object carries text, createdAt, likeCount, retweetCount, replyCount, quoteCount, viewCount, bookmarkCount, entities, extendedEntities.media and an embedded author, instead of the nested core.user_results.result wrappers twscrape has to unpack. Third, nothing in the request changes when X ships a new web build; there is no query ID or feature flag on your side.
Writes. twikit's write operations (CreateTweet, FavoriteTweet, CreateRetweet) also have counterparts, but they work differently because a write has to act as an account. One /twitter/user_login_v2 login ($0.10, with a proxy you supply) returns login_cookies; each post, like, retweet or follow is then a $0.002 call to /twitter/create_tweet_v2, /twitter/like_tweet_v2, /twitter/retweet_tweet_v2 or /twitter/follow_user_v2. Those calls act as that account, so X can rate-limit, lock or suspend it; keep a human pace, expect the session to need a re-login now and then, and note that each HTTP 200 call is billed.
Field coverage and latency: five operations, measured
A mapping table is only useful if the replacement returns what your GraphQL parser used to extract. twscrape's models.py is a good yardstick: its Tweet and User classes are exactly the fields that library pulls out of GraphQL responses. We took the 25 data fields of Tweet and 20 of User (leaving out duplicate string IDs and URLs it builds itself) and checked which come back from the published endpoint.
Tweets. Every core field was present in all 450 tweets across the three tweet endpoints: id, URL, creation time, author, language, text, the five counters (reply, retweet, like, quote, bookmark), conversation id and view count, plus entities, extendedEntities, retweeted_tweet, quoted_tweet, place and the reply fields. Only coordinates has no counterpart (precise geotags are rare on X anyway; place stayed empty on all 450). One gap worth knowing: the source key (the posting client) is present but came back empty on all 450 tweets.
Profiles. /twitter/user/info returned 19 of the 20 User fields on all 10 profiles, including isBlueVerified and verifiedType (twscrape's blue and blueType), pinnedTweetIds, coverPicture and profilePicture. Only the listed count is absent. In the author object embedded in each tweet the bio sits under profile_bio.description (populated on 100% of authors) while the top-level description was empty, and the protected flag is not included; a parser porting from twscrape should read those two from /twitter/user/info instead.
Follower rows. /twitter/user/followers returns snake_case rows built for bulk export (200 per page): id, handle, name, bio, location, creation date, the follower, following, post, like and media counts, avatar, banner, protected and verified. It leaves out the blue-check type, bio links and pinned posts; if you need those for a subset, call /twitter/user/batch_info_by_ids on their ids.
Latency. These are client-side times from one machine, from request sent to body parsed, across 10 calls per endpoint (5 for followers). A single profile took a median of 1,229 ms; a 20-tweet page took 2.3 to 2.5 seconds; a 200-row follower page took 2,311 ms. For collection jobs, page count and your rate limit, not per-call latency, decide the run time. Throughput is set by your plan's rate limit: 0.2 requests per second before the first payment, 20 once paid, 50 on Scale, 500 on Business.

The three stable ways to get the same data, and what each costs
If the private GraphQL API is off the table, three routes remain. They differ in who holds the X account, who absorbs changes to x.com, and how you pay.
Official X API. X's pricing page at docs.x.com/x-api/getting-started/pricing lists pay-per-use reads at $0.005 per post, $0.010 per user and $0.010 per follower or following returned, and $0.015 per post created ($0.20 with a URL). Pay-per-usage is capped at 3 million post reads per monthly billing cycle, with Enterprise above that. New accounts get $20 in X API credits when they save a card and buy credits, and a match of the first auto-recharge up to $50. Per 1,000 posts read, that is $5 on X's rate card against $0.15 on ours, about 33x.
twitterapi.io. Tweet endpoints cost 15 credits per tweet returned ($0.15 per 1,000 tweets), with a 15-credit minimum per request, so a search that finds nothing costs $0.00015 and a full 20-tweet page $0.003. Profiles are 18 credits each ($0.18 per 1,000). $1 buys 100,000 credits; new accounts that pass the automatic bot check get up to $1 of free credits (at most 100,000 credits, no card, one per person or network, expiring after 31 days), and purchased credits never expire. Monthly plans run from Starter at $29 to Business at $1,499; see the pricing page for what each includes.
Our own study as a price check. The 45 requests behind this page returned 450 tweets and 10 profiles for 6,930 credits ($0.0693), plus 1,000 follower rows at 1 to 3 credits each ($0.01 to $0.03). The same reads at X's pay-per-use prices would be $12.35, most of it the $0.010 per follower.
Porting a GraphQL client: a checklist
Replace operations one by one. Use the mapping table: swap UserByScreenName for /twitter/user/info, UserTweets for /twitter/user/last_tweets, SearchTimeline for /twitter/tweet/advanced_search, and so on. Delete the query-ID constants, the features dictionary, the bearer token and the transaction-ID code; none of it has a counterpart.
Rewrite pagination. GraphQL timelines hide the next cursor among the timeline entries, as an entry whose entryId starts with cursor- and whose cursorType is Bottom (twscrape's _get_cursor). The published endpoints return has_next_page and next_cursor at the top level (for /twitter/user/last_tweets both sit in data next to tweets); loop until has_next_page is false.
Rename fields. twscrape's rawContent is text, date is createdAt (format Thu Oct 16 12:34:56 +0000 2026), bookmarkedCount is bookmarkCount, user is author, followersCount is followers and friendsCount is following on profile objects. Follower rows use snake_case (followers_count, screen_name).
Search operators carry over. SearchTimeline takes X's search syntax in rawQuery; /twitter/tweet/advanced_search takes the same operators in query (from:, since_time:, lang:, min_faves: and so on), with queryType=Latest or Top. In our study a from: search for each account returned 20 posts per page, 30% of them replies (the timeline endpoint without includeReplies returned none).
Handle the not-found case. An unknown handle on /twitter/user/info returns HTTP 200 with status: "error" and msg: "user not found", not a 404, so check status before reading data.
Retire the accounts. Once reads move to the published API, the X accounts and proxies that fed the GraphQL client are no longer needed for reads. If you also post, keep one account and route writes through a login session as described above, at a human pace.
How we collected this
Question. Which fields that twscrape's GraphQL parser extracts come back from the published equivalent of five common operations, and how long does each call take? And from public source history only: how often did the GraphQL query IDs hard-coded in a maintained client have to change?
Accounts and calls. 10 organization accounts: @XDevelopers, @github, @vercel, @OpenAI, @nvidia, @NASA, @Docker, @stripe, @Cloudflare and @SpaceX. Per account: one /twitter/user/info, one page of /twitter/user/last_tweets, one page of /twitter/tweet/advanced_search with from:<account> and queryType=Latest, and one /twitter/tweets call for the five newest timeline ids. For the first five accounts, one 200-row page of /twitter/user/followers. 45 requests in total, collected in 2026, through our rate-limited read client.
Source history. Every version of twscrape/api.py committed since the file was created in mid-2024 (40 versions) from github.com/vladkens/twscrape, parsed for OP_<Name> = "<queryId>/<Name>" constants. No x.com GraphQL endpoint was called for this page.
What the sample cannot show. Latency comes from one client location and one session, so treat it as indicative, not a service-level figure. Field coverage is measured against twscrape's model, not against every field a raw GraphQL response can contain; GraphQL responses carry more internal fields (feature-gated objects, ad and ranking metadata) than any parser keeps. The query-ID count is a lower bound, as explained above.
Privacy. Aggregates only. Only organization accounts are named; follower rows (private individuals) were reduced to field-presence counts and were not stored. The study files (protocol, analysis script, results, de-identified sample and compressed raw responses for the organization accounts) are kept with the page source.
"""Port of a typical GraphQL scraping flow (UserByScreenName -> UserTweets -> TweetResultsByRestIds)
to published REST endpoints. No query IDs, feature flags, cookies or X account needed."""
import os
import requests
BASE = "https://api.twitterapi.io"
HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}
def user_by_screen_name(user_name):
# replaces the UserByScreenName GraphQL operation
r = requests.get(f"{BASE}/twitter/user/info", params={"userName": user_name}, headers=HEADERS, timeout=30)
r.raise_for_status()
body = r.json()
if body.get("status") != "success" or not body.get("data"):
raise LookupError(body.get("msg", "user not found"))
return body["data"]
def user_tweets(user_name, max_pages=3, include_replies=False):
# replaces UserTweets (or UserTweetsAndReplies with include_replies=True)
params = {"userName": user_name, "includeReplies": str(include_replies).lower()}
for _ in range(max_pages):
r = requests.get(f"{BASE}/twitter/user/last_tweets", params=params, headers=HEADERS, timeout=30)
r.raise_for_status()
data = r.json().get("data") or {}
for tweet in data.get("tweets", []):
yield tweet
if not data.get("has_next_page") or not data.get("next_cursor"):
break
params["cursor"] = data["next_cursor"]
def search_timeline(query, query_type="Latest", max_pages=2):
# replaces SearchTimeline (Latest or Top tab)
params = {"query": query, "queryType": query_type}
for _ in range(max_pages):
r = requests.get(f"{BASE}/twitter/tweet/advanced_search", params=params, headers=HEADERS, timeout=30)
r.raise_for_status()
body = r.json()
for tweet in body.get("tweets", []):
yield tweet
if not body.get("has_next_page") or not body.get("next_cursor"):
break
params["cursor"] = body["next_cursor"]
def tweets_by_ids(ids):
# replaces TweetResultByRestId / TweetResultsByRestIds
r = requests.get(f"{BASE}/twitter/tweets", params={"tweet_ids": ",".join(ids)}, headers=HEADERS, timeout=30)
r.raise_for_status()
return r.json().get("tweets", [])
if __name__ == "__main__":
user = user_by_screen_name("XDevelopers")
print(user["userName"], user["followers"], "followers,", user["statusesCount"], "posts")
timeline = list(user_tweets("XDevelopers", max_pages=1))
for t in timeline[:5]:
print(t["createdAt"], t["likeCount"], "likes |", t["text"][:80].replace("\n", " "))
fresh = tweets_by_ids([t["id"] for t in timeline[:3]])
print("re-fetched", len(fresh), "tweets; view counts:", [t.get("viewCount") for t in fresh])
hits = list(search_timeline("from:XDevelopers api", max_pages=1))
print(len(hits), "search results on the first page")
Questions readers ask
Does Twitter (X) have a public GraphQL API?
No. X uses GraphQL internally: x.com's web client calls endpoints under x.com/i/api/graphql/<queryId>/<Operation>. X does not document them, issue keys for them or promise they will stay the same. The public, documented interface is the X API at docs.x.com, which is REST (/2/... paths).
What is a Twitter GraphQL query ID, and why does it change?
It is the hash before the operation name in the URL, for example OdBLACRZ0-9NikpJotaT7g in OdBLACRZ0-9NikpJotaT7g/UserTweets. It identifies one compiled version of the query in x.com's JavaScript bundles, so it can change when X ships a new web build. In twscrape's public history, UserTweets, SearchTimeline, TweetDetail and Followers each went through 15 different IDs between mid-2024 and October 2026. A stale ID gets an HTTP 404 with an empty body (twscrape issue #306).
What is the Twitter guest token?
A short-lived token the web client obtains for logged-out visitors and sends as the X-Guest-Token header alongside the web client's bearer token; twikit's guest client still implements it. Since X's 2023 login wall, most useful operations need a logged-in session (the auth_token and ct0 cookies) instead, which is why twscrape requires authorized accounts.
What is x-client-transaction-id?
A header X began requiring on its web API calls in 2026. It is computed in the browser from a script x.com serves, and per twscrape issue #306 requests without it get a 404. Open-source clients now reimplement the computation (twscrape's xclid.py), which is one more moving part that can break.
Is it legal to use the Twitter GraphQL API?
X's Terms of Service (effective October 9, 2026) prohibit accessing the Services "by any means (automated or otherwise) other than through our currently available, published interfaces" without a separate agreement, prohibit crawling or scraping without prior written consent, and set liquidated damages of $15,000 per 1,000,000 posts accessed in 24 hours. The private GraphQL endpoints are not a published interface. This is not legal advice; read the terms and talk to counsel for your case.
Can my X account get locked for calling GraphQL endpoints from code?
Yes. Every logged-in GraphQL call runs as a specific account, and that account is what X rate-limits, locks or suspends. twscrape's README notes that X's terms discourage multiple accounts, and its issue tracker has many locked-account and 403 reports.
What can I use instead of the Twitter GraphQL API?
The official X API (pay per use: $0.005 per post read, $0.010 per user read, capped at 3 million post reads a month) or a managed REST API such as twitterapi.io, which maps each GraphQL read operation to a fixed endpoint (/twitter/user/info, /twitter/user/last_tweets, /twitter/tweet/advanced_search, /twitter/tweets, /twitter/user/followers) at $0.15 per 1,000 tweets, with one X-API-Key header and no X account needed for reads.
Does the replacement return the same fields as GraphQL?
Nearly. In our study the published endpoints returned 24 of the 25 tweet fields twscrape parses from GraphQL (all but coordinates) and 19 of 20 profile fields (all but listedCount). Field names are flatter: text instead of a nested full_text, and an author object instead of core.user_results.result.
Continue
- twscrape source, api.py: GQL_URL, OP_ query-ID constants, GQL_FEATURES (GitHub)
- twscrape issue #306: stale query IDs return 404; x-client-transaction-id now required (GitHub)
- twikit source, gql.py: GraphQL operation list (GitHub)
- X Terms of Service (effective October 9, 2026): Misuse of the Services and liquidated damages
- X API pricing: pay-per-use read and write rates
- twitterapi.io docs: Get User Last Tweets
- X API overview: endpoints, pricing and access
- twscrape review: the account pool, locks and migration
- twikit: the cookie-based Twitter client and its limits
- How to scrape Twitter (X) without the official API
- Full map of Twitter (X) API endpoints
- TwitterAPI.io vs the official X API, compared
- twitterapi.io pricing and credit plans
- twitterapi.io API reference
- 01GetXBot: What the Twitter (X) Video Bot Is, Whether It Is Safe, and Alternatives
- 02The Best Twitter (X) Accounts to Follow, by Topic
- 03What Is xcancel (X Cancel)? The Twitter (X) Nitter Front End, Explained
- 04Twitter (X) Media Viewer: How to Browse an Account's Photos and Videos
- 05AllMyTweets: What It Did, Its Limits, and How to See Every Tweet on Twitter (X)
- 06X Post Templates That Actually Work
- 07How to Find Deleted Twitter (X) Accounts: What Still Exists and Where to Look
- 08Twitter (X) Follower Count — API Tutorial
Stop reading. Start building.
Starter credits cover real testing on real data. Google sign-in, no card, no application queue.
Get an API key