Twitter (X) OpenAPI: Which Spec to Use and How to Generate a Typed Client

- Three specs share the name. X official v2 (
api.x.com/2/openapi.json, OpenAPI 3.0.0, version 2.174, 259 operations), the community fa0311/twitter-openapi spec of the internal GraphQL API (93 operations, cookie auth), and the TwitterAPI.io spec (docs.twitterapi.io/api-reference/openapi.json, 84 operations,X-API-Key). - Auth is the real difference. Of X's 259 operations, 88 accept an app-only Bearer token and 170 need user context (OAuth 2.0 user token or OAuth 1.0a); the community spec needs a logged-in browser session's
ct0andauth_tokencookies; ours needs one API key header. - Codegen works on all three, with care:
npx openapi-typescript(7.13.0) turned our spec into a 6,147-line.d.ts;datamodel-codegen(0.90.0) produced pydantic v2 models in which every Tweet field is optional. - Real responses beat the spec: across 1,204 posts all 30 tweet keys were present every time, 22 were always non-null,
cardwas filled on only 28.4% andquoted_tweeton 8.1%;extendedEntitiesis always an object but carries media on only 31.0%. - Check the drift: 6 keys in real responses (
extendedEntities,card,article,place,twitterUrl,communityInfo) are not declared in our spec's Tweet schema, and a default pydantic model silently drops them. Setextra='allow'or add them. - Cost of the study: 1,204 posts at $0.15 per 1,000 tweets returned came to about $0.18; the same reads at X's pay-per-usage rate card ($0.005 per post read) would be about $6.02.
Search for "twitter openapi" and you land on a mix of things that look alike and are not: a live JSON endpoint on api.x.com, a GitHub repository that reverse-documents the x.com web app, an archived copy on an API directory, and SDK READMEs. They are all OpenAPI files, but they describe different APIs with different hosts, auth, licenses and break risk. Pick the wrong one and your generated client either cannot authenticate or breaks the next time the web app ships.
This page separates the three, shows what each file actually contains (we parsed all three and counted operations, schemas and auth schemes), and walks through generating a typed client in TypeScript and Python. If you want the background on the private web API itself, our explainer on the Twitter GraphQL API covers it; if you want a map of every endpoint by task, see the Twitter (X) API endpoints map. This page stays on the spec files and code generation.
Then the part a spec cannot tell you: we pulled 1,204 recent posts from 10 organization accounts in the API-tooling space (including @XDevelopers and @API) and measured, key by key, what a real tweet object contains: which fields are always there, which are null, which are present but empty, and which the spec forgot. Those numbers decide whether a generated model is safe to use as is.
30 of 30, yet only 22 were always non-null, and 6 of the 30 are not declared in the spec's Tweet schema (1,204 posts from 10 organization accounts, collected in 2026).
The three Twitter OpenAPI specs at a glance
Each spec below was fetched and parsed this year; the counts come from the files themselves, not from documentation pages.
Which one should you use? If you already have X developer access and want first-party behavior, use X's spec, or skip codegen and install XDK, which X generates from that same file. If you only need to read public data and do not want an X developer account, the TwitterAPI.io spec is the shortest path. The community spec is a reverse-engineering effort of real technical interest, but it automates a logged-in personal session against undocumented endpoints; read its license and X's terms before building on it.
A note on old copies. API directories still mirror an older X file: the APIs.guru listing for twitter.com shows version 2.61, last updated in February 2023, while the live file reports 2.174. Always generate from the live URL, not a mirror.

X's official OpenAPI spec: api.x.com/2/openapi.json
X documents the file as an endpoint of its own: GET /2/openapi.json "retrieves the full OpenAPI Specification in JSON format" (docs.x.com, Get OpenAPI Spec). It needs no auth, and it is the only operation in the file with no security requirement. The download is about 1.3 MB of JSON.
What is inside. OpenAPI 3.0.0, info.version 2.174, title "X API v2", one server (https://api.x.com), 201 paths and 259 operations: 125 GET, 77 POST, 40 DELETE and 17 PUT. Every operation has an operationId, which is what gives generated clients readable method names. There are 886 component schemas. The post object is now named Post in the components (36 properties, from id, text and author_id to public_metrics, edit_history_post_ids and note_post), and it declares no required properties, so generated models treat every field as optional.
Auth per operation. The file defines three security schemes: BearerToken (app-only), OAuth2UserToken and UserToken (OAuth 1.0a). Counting each operation's security list: 88 operations accept an app-only Bearer token, 170 require user context, and 1 (the spec itself) needs none. A generated client therefore needs to know which token to send per call.
Generated SDKs. X's own generator, XDK (github.com/xdevplatform/xdk, MIT license), is a Rust tool that "creates language-specific SDKs for the X API from OpenAPI specifications". Its outputs are published as xdk on PyPI (0.10.6) and @xdevplatform/xdk on npm (0.6.6), the versions current when we checked. The older hand-written twitter-api-typescript-sdk repository was last pushed in January 2024.
Cost if you call it. The spec is free; the calls it describes are billed on X's pay-per-usage rate card (docs.x.com/x-api/getting-started/pricing): $0.005 per post read, $0.010 per user read, with pay-per-usage capped at 3M post reads a month and Enterprise above that.
# Download X's live spec and generate a Python client with openapi-generator (needs Java)
curl -s https://api.x.com/2/openapi.json -o x-openapi.json
python3 -c "import json; d=json.load(open('x-openapi.json')); print(d['info']['version'], len(d['paths']), 'paths')"
npx @openapitools/openapi-generator-cli generate \
-i x-openapi.json -g python -o ./x_client \
--additional-properties=packageName=x_client
# Or skip codegen: X publishes clients generated from this file
pip install xdk # Python
npm install @xdevplatform/xdk # TypeScriptThe community spec: fa0311/twitter-openapi and the internal GraphQL API
The GitHub repository fa0311/twitter-openapi describes itself as the "OpenAPI(Swagger) specification of Twitter Internal API (Twitter graphql API)". It is not an X product. Its spec file (dist/docs/openapi-3.0.yaml, about 850 KB) declares OpenAPI 3.0.3, servers https://x.com/i/api and https://twitter.com/i/api, and 93 operations: 86 under /graphql/{pathQueryId}/... (for example AboutAccountQuery or AudioSpaceById) and 7 older REST paths such as /1.1/friendships/create.json and /2/search/adaptive.json.
Auth. The file declares 23 security schemes, most of them headers the web client sends (SecChUa, ClientTransactionId, UserAgent) alongside the session cookies ct0 and auth_token and a CSRF token. In practice that means every request runs as a logged-in X account. The README also points to a companion relay that runs requests through a logged-in browser.
Activity and packages. The repository is active: it was last pushed in November 2026, when a commit recorded that 39 operations had moved to the web app's current query versions. Generated clients exist for Python (twitter-openapi-python on PyPI, 0.1.6) and TypeScript (twitter-openapi-typescript on npm, 0.1.6), plus a Dart client whose repository has not been pushed since February 2024. A separate fork, thecodrr/twitter-openapi, has not been pushed since June 2023.
License. The spec's info.license is LicenseRef-Custom OR AGPL-3.0-or-later, and the README notes that "the license also inherits to the output", so code you generate from it carries the same terms. That matters for any closed-source product.
Why break risk is high. GraphQL query IDs and feature flags are whatever the x.com frontend sends this week. The project updates them as the web app changes, but your deployed client only follows if you regenerate and redeploy. For the full picture of how that web API works and why it moves, read our explainer on the Twitter GraphQL API; we do not repeat it here.
The TwitterAPI.io OpenAPI spec
Our docs publish the spec at https://docs.twitterapi.io/api-reference/openapi.json, and the docs' machine-readable index at docs.twitterapi.io/llms.txt lists it under "OpenAPI Specs". It is OpenAPI 3.0.1, version 1.0.0, MIT licensed, with one server (https://api.twitterapi.io) and one security scheme: ApiKeyAuth, an X-API-Key header.
What is inside. 84 operations: 75 on the twitter path prefix (search, timelines, users, followers, lists, communities, spaces, trends; for example /twitter/tweet/advanced_search) and 8 on the oapi prefix (filter rules such as /oapi/tweet_filter/add_rule and account monitoring that push to your webhook). By method: 37 GET, 40 POST, 3 PATCH, 3 DELETE and 1 PUT. The Tweet schema declares 24 properties; the user objects are UserInfo (camelCase) and UserProfileSnake (snake_case, the shape follower lists return).
Two things to know before you generate. First, the operations carry no operationId, so tools that name methods from it fall back to names built from the path and method; openapi-typescript is unaffected because it keys types by the literal path ("/twitter/user/last_tweets"). Second, real tweet objects carry 6 keys the Tweet schema does not declare; the next sections measure that and show the fix.
Writes are in the spec too. Many of the POST operations are write actions (/twitter/create_tweet_v2 and others). They run through an authenticated session: one /twitter/user_login_v2 login ($0.10, with a proxy you supply) returns login_cookies, and each post, delete, like, retweet, follow or similar action is then 60 credits ($0.0006). Such a call acts as that account, so X can rate-limit, lock or suspend it; keep a normal human pace, expect the session to need a re-login sometimes, and note that billing is per HTTP 200 call. Reads need no X account at all.
Price, for comparison. Tweet endpoints are pay-per-use at $0.15 per 1,000 tweets returned (15 credits per tweet), with a 15-credit minimum per request, so a search that finds nothing costs $0.00015. X's pay-per-usage rate card (docs.x.com/x-api/getting-started/pricing) lists $0.005 per post read, which is $5 per 1,000, about 33x more. 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); see the pricing page for plans.
What a real tweet object contains: 1,204 posts, key by key
A spec says what may appear; a response says what does. We pulled the latest posts of 10 organization accounts through /twitter/user/last_tweets (1,204 posts, 65 requests) and checked every top-level key of every tweet object: is the key there, is the value non-null, is it non-empty, and which JSON types appear.
The shape is stable. All 30 keys were present on all 1,204 objects. No key ever disappeared, so a typed model can declare all 30 instead of guessing.
The values are not. 22 keys were non-null on every post. 6 were nullable: retweeted_tweet (an object on 36.0% of posts), card (28.4%), quoted_tweet (8.1%), article (1.1%), and inReplyToUserId / inReplyToUsername (0.3%). Two were null on every post in this sample: communityInfo and inReplyToId.
Present but empty is its own case. extendedEntities and place are never null: they are objects that are empty when there is nothing to report. A model that marks them Optional[...] = None passes validation but sends you looking for None when the real signal is a missing media list. source is always an empty string in this sample; treat it as unused rather than as a posting-client signal.
Reply fields do not move together. On 4 posts (0.3%) inReplyToUserId and inReplyToUsername were set while isReply was false and inReplyToId was null: posts that open a new conversation by addressing someone. Model the three fields independently and use isReply when you mean "is this a reply".

Spec drift: keys the schema does not declare, and what codegen does with them
Comparing the 30 keys in real responses with the 24 properties of the Tweet schema in our spec, 6 are missing from the schema: extendedEntities, card, article, place, twitterUrl and communityInfo. Nothing in the spec claims to be exhaustive (no additionalProperties: false), so this is legal OpenAPI, but it changes what your generated code does.
Python. We generated pydantic v2 models with datamodel-codegen 0.90.0 and validated a real tweet that carried media. The model accepted it, but pydantic v2 ignores undeclared keys by default, so all 6 undeclared keys, including extendedEntities with the photo or video, were silently dropped. Two fixes work: set model_config = ConfigDict(extra="allow") on the base model, or add the 6 fields to a subclass. The generator also warned that 10 of 44 class names were numbered collisions (Url1 to Url8), so expect a few awkward class names.
TypeScript. openapi-typescript 7.13.0 produced a 6,147-line .d.ts. Types are erased at runtime, so nothing is dropped, but the compiler will not let you read tweet.extendedEntities without a cast. Declare a local extension type once.
X's spec has the same lesson in a different form. Its Post schema declares no required properties, so every generated field is optional, and what you actually receive depends on the tweet.fields and expansions you request. Generated types tell you what can exist, not what will.
The rule. Generate, then validate one real response per endpoint you use, and diff its keys against the model. The runnable script at the end of this page does exactly that for our spec.
# pip install datamodel-code-generator pydantic requests
# datamodel-codegen --input openapi.json --input-file-type openapi \
# --output-model-type pydantic_v2.BaseModel --output models.py
from typing import Any, Optional
from pydantic import ConfigDict
from models import Tweet as GeneratedTweet
class Tweet(GeneratedTweet):
# keep anything the spec does not declare instead of dropping it
model_config = ConfigDict(extra="allow")
twitterUrl: Optional[str] = None
extendedEntities: dict[str, Any] = {} # {} when the post has no media
card: Optional[dict[str, Any]] = None
article: Optional[dict[str, Any]] = None
place: dict[str, Any] = {} # almost always {}
communityInfo: Optional[dict[str, Any]] = None
def media_urls(t: Tweet) -> list[str]:
return [m.get("media_url_https", "") for m in t.extendedEntities.get("media", [])]Generate a typed TypeScript client from the spec
For TypeScript the lightest path is openapi-typescript (types only) plus openapi-fetch (a small typed fetch wrapper). Both read paths and parameters from the spec, so they do not need operationIds. The versions we used were openapi-typescript 7.13.0 and openapi-fetch 0.17.0.
Step 1. Generate the types into your repo and commit them, so a spec change shows up as a diff in review rather than as a runtime surprise.
Step 2. Create the client with the base URL and the X-API-Key header, then call endpoints by their literal path. Parameters and the response body are typed from the spec; the undeclared keys you need are added with an intersection type.
Step 3. Paginate with has_next_page and next_cursor. For /twitter/user/last_tweets the tweets are in data.tweets, 20 per page, newest first; search endpoints such as /twitter/tweet/advanced_search return tweets at the top level.
If you prefer full generated clients with methods (axios or fetch based), openapi-generator with -g typescript-fetch works on all three specs; on ours, expect method names derived from paths.
// npx openapi-typescript https://docs.twitterapi.io/api-reference/openapi.json -o src/twitterapi.d.ts
// npm install openapi-fetch
import createClient from "openapi-fetch";
import type { paths, components } from "./twitterapi";
type Tweet = components["schemas"]["Tweet"] & {
twitterUrl?: string;
extendedEntities?: { media?: { type: string; media_url_https: string }[] };
card?: Record<string, unknown> | null;
article?: Record<string, unknown> | null;
};
const client = createClient<paths>({
baseUrl: "https://api.twitterapi.io",
headers: { "X-API-Key": process.env.TWITTERAPI_IO_KEY ?? "" },
});
export async function lastTweets(userName: string, maxPages = 3): Promise<Tweet[]> {
const out: Tweet[] = [];
let cursor = "";
for (let i = 0; i < maxPages; i++) {
const { data, error } = await client.GET("/twitter/user/last_tweets", {
params: { query: { userName, cursor } },
});
if (error || !data) throw new Error(JSON.stringify(error));
const body = data as any; // tweets live in body.data.tweets
out.push(...((body.data?.tweets ?? []) as Tweet[]));
if (!body.has_next_page) break;
cursor = body.next_cursor;
}
return out;
}Who announces spec changes: @XDevelopers and @API
A spec is only as current as the last time you regenerated, so it helps to know where changes are announced. Two of the 10 accounts in our sample are X's own developer accounts, and their timelines show where the news now lives.
@XDevelopers is the live channel. 128 of its 129 sampled posts are from 2026. Most of them (82.2%) are reposts, so following it means following the accounts it amplifies, but 17 of its 23 own posts mention API, SDK, endpoint, pricing or similar terms.
@API has gone quiet. Its sample spans 2018 to 2025, with 16 to 22 posts a year from 2018 to 2021, then 13, 7, 8 and 5 a year from 2022 to 2025, and its newest post is from November 2025, 359 days before we collected. Do not rely on it for change notices.
For the community spec, the signal is the repository itself: watch its commits, because a query-version refresh there is exactly the event that breaks a deployed client. For the TwitterAPI.io spec, the changelog and the docs index (docs.twitterapi.io/llms.txt) are the places to watch.
How we collected this
Question. What does a real tweet object contain, key by key, compared with what the specs declare; and how actively do X's developer accounts post about API changes?
Specs. We downloaded api.x.com/2/openapi.json, dist/docs/openapi-3.0.yaml from fa0311/twitter-openapi and docs.twitterapi.io/api-reference/openapi.json in 2026 and counted paths, operations by method, component schemas and security schemes, plus the security requirement of every X operation.
Accounts. The latest posts of 10 organization accounts: @XDevelopers, @API, @OpenAPISpec, @swaggerapi, @getpostman, @github, @vercel, @Docker, @stripedev and @CloudflareDev, through /twitter/user/last_tweets (replies excluded, the endpoint's default), up to 7 pages each. Sample: 1,204 posts, 65 requests, collected in 2026. The window per account runs from its oldest to newest sampled post and varies widely, because some accounts post daily and others rarely.
Measures. For each top-level key of each tweet and author object: present, non-null, non-empty, JSON types seen. Codegen was checked with openapi-typescript 7.13.0 and datamodel-code-generator 0.90.0 on our spec.
What the sample cannot show. Replies were excluded, so reply-specific fields are underrepresented; organization accounts post more media and cards than typical users; other endpoints (search, mentions) return the same tweet object, but we did not measure them here. The @API and @XDevelopers topic shares use a simple keyword match on post text.
Cost. 1,204 posts at 15 credits each is about $0.18 at TwitterAPI.io prices. The same 1,204 post reads at X's pay-per-usage rate ($0.005 each) would be about $6.02.
Privacy. Aggregates only, organization accounts only, no analysis of reposted or quoted authors. Field presence counts keys, not content.
"""Download the TwitterAPI.io OpenAPI spec, call one endpoint, and diff a real
tweet object against the spec's Tweet schema.
pip install requests
export TWITTERAPI_IO_KEY=...
"""
import os
import requests
BASE = "https://api.twitterapi.io"
SPEC_URL = "https://docs.twitterapi.io/api-reference/openapi.json"
HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}
# 1. Read the spec: operations and the declared Tweet properties
spec = requests.get(SPEC_URL, timeout=30).json()
ops = [(m.upper(), p) for p, item in spec["paths"].items()
for m in item if m in ("get", "post", "put", "patch", "delete")]
print(f"{spec['info']['title']} {spec['info']['version']}: {len(ops)} operations")
declared = set(spec["components"]["schemas"]["Tweet"]["properties"])
# 2. Fetch real tweets (20 per page, newest first), following the cursor
tweets, cursor = [], ""
for _ in range(3):
r = requests.get(
f"{BASE}/twitter/user/last_tweets",
headers=HEADERS,
params={"userName": "XDevelopers", "cursor": cursor},
timeout=30,
)
r.raise_for_status()
body = r.json()
if body.get("status") != "success":
raise SystemExit(body.get("msg"))
tweets += (body.get("data") or {}).get("tweets") or []
if not body.get("has_next_page"):
break
cursor = body.get("next_cursor", "")
# 3. Diff the real keys against the schema and report null / empty values
seen = set().union(*(t.keys() for t in tweets)) if tweets else set()
print(f"{len(tweets)} tweets, {len(seen)} keys")
print("in responses, not in the schema:", sorted(seen - declared))
print("in the schema, never seen:", sorted(declared - seen))
for key in sorted(seen):
null = sum(1 for t in tweets if t.get(key) is None)
empty = sum(1 for t in tweets if t.get(key) in ("", [], {}))
if null or empty:
print(f" {key:20} null {null:3} empty {empty:3} of {len(tweets)}")
# 4. Media lives in extendedEntities (an empty object when there is none)
for t in tweets[:5]:
media = (t.get("extendedEntities") or {}).get("media") or []
print(t["createdAt"], t["likeCount"], [m.get("type") for m in media])Researching this?Free starter credits cover end-to-end testing — Google sign-in, no card. Get a key.
Questions readers ask
Does X (Twitter) publish an official OpenAPI spec?
Yes. X serves its API v2 spec at https://api.x.com/2/openapi.json and documents it as the Get OpenAPI Spec endpoint on docs.x.com. It needs no auth. When we parsed it in 2026 it was OpenAPI 3.0.0, version 2.174, with 259 operations and 886 schemas.
What is fa0311/twitter-openapi?
A community project on GitHub that writes an OpenAPI spec for the private GraphQL endpoints the x.com web app uses (93 operations, 86 of them GraphQL). It requires a logged-in session's cookies, changes whenever the web app's query versions change, and is dual licensed under a custom license or AGPL-3.0, a license its README says carries over to generated code. It is not an X product.
How do I generate a Python client from the Twitter OpenAPI spec?
For models, run datamodel-codegen --input openapi.json --input-file-type openapi --output-model-type pydantic_v2.BaseModel --output models.py; for a full client, run openapi-generator with -g python. For X's own API you can also install xdk, which X generates from its spec. With pydantic, set extra="allow" or declare missing fields, because undeclared keys are dropped by default.
How do I generate TypeScript types from the Twitter OpenAPI spec?
Run npx openapi-typescript <spec-url> -o twitterapi.d.ts and use openapi-fetch for a typed fetch client keyed by path. On the TwitterAPI.io spec this produced a 6,147-line type file with openapi-typescript 7.13.0. For X's API, the @xdevplatform/xdk npm package is generated from X's spec.
Is there an OpenAPI spec for TwitterAPI.io?
Yes, at https://docs.twitterapi.io/api-reference/openapi.json, listed in the docs index docs.twitterapi.io/llms.txt. It covers 84 operations with a single X-API-Key header. Real tweet objects carry 6 keys the Tweet schema does not declare (extendedEntities, card, article, place, twitterUrl, communityInfo), so extend generated models with them.
Why is the APIs.guru copy of the Twitter spec different?
It is an old snapshot. The APIs.guru listing for twitter.com shows version 2.61, last updated in February 2023, while X's live file reported version 2.174 when we checked. Generate from the live URL.
Which Twitter OpenAPI fields can be null?
In our sample of 1,204 posts, retweeted_tweet, card, quoted_tweet, article, inReplyToUserId, inReplyToUsername, communityInfo and inReplyToId were null on some or all posts. extendedEntities and place are never null but are often empty objects, and source was always an empty string.
Continue
- docs.x.com: Get OpenAPI Spec (GET /2/openapi.json, the live X API v2 spec)
- xdevplatform/xdk on GitHub: X's SDK generator that builds Python and TypeScript SDKs from the OpenAPI spec
- fa0311/twitter-openapi on GitHub: community OpenAPI spec of the internal GraphQL API (README, license, commit history)
- TwitterAPI.io OpenAPI spec (OpenAPI 3.0.1, X-API-Key auth)
- openapi-typescript and openapi-fetch on GitHub: TypeScript types and a typed fetch client from OpenAPI
- docs.x.com: X API pricing (pay-per-usage rate card)
- X (Twitter) API overview: endpoints, pricing and access
- Twitter GraphQL API: what the internal web API is and why it changes
- Twitter (X) API endpoints: the full map by task
- twitter-api-v2 on npm: a Node quick start
- Twitter (X) API with Python: four paths with code
- llms.txt for API docs: X's file compared with ours
- TwitterAPI.io API reference and OpenAPI spec
- TwitterAPI.io pricing and plans
- 01Zapier Pricing in 2026: Plans, Tasks per Month, Overage and a Worked Twitter (X) Zap Bill
- 02LlamaIndex vs LangChain: Which to Use, With the Same Twitter (X) RAG Pipeline Built in Both
- 03Agentic Payments: How AI Agents Pay, and Which Protocol Does What
- 04Kiro Pricing in 2026: Plans, Credits, Model Multipliers and Worked Monthly Bills
- 05Cline vs Cursor in 2026: Cost, Models, Plan/Act and MCP Compared
- 06MCP Server Workflow Automation: the 14 Best MCP Servers for Claude Code, Codex and Cursor, Twitter (X) Data Included
- 07Claude in Chrome: What It Is, How to Set It Up, and When an API Beats It for Twitter (X) Data
- 08Does X / Twitter Show Who Viewed Your Profile?
Writing about something you built with TwitterAPI.io? Our community rewards program pays API credits for honest tutorials, build logs and videos.
Stop reading. Start building.
Starter credits cover real testing on real data. Google sign-in, no card, no application queue.
Get an API keyAlready building with TwitterAPI.io? Publish an honest tutorial or build log about it and earn up to $200 a month in API credits.