Simple format
Opt-in compact JSON for every read endpoint. Add format=simple and get flat, camelCase tweets, users, lists, communities and jobs with a fixed schema and a cursor that is null on the last page.
By default every endpoint returns X's raw JSON (see Response format). That does not change. If you would rather work with small, flat objects, add format=simple to the query string, or send the header X-Response-Format: simple:
curl 'https://api.twittrapi.com/user/2244994945/tweets?count=20&format=simple' \
--header 'X-API-KEY: YOUR_API_KEY'curl 'https://twitter-aio.p.rapidapi.com/user/2244994945/tweets?count=20&format=simple' \
--header 'x-rapidapi-host: twitter-aio.p.rapidapi.com' \
--header 'x-rapidapi-key: YOUR_API_KEY'{
"data": [
{
"id": "2019881223666233717",
"url": "https://x.com/XDevelopers/status/2019881223666233717",
"text": "Officially launching X API Pay-Per-Use … We’re so back. http://developer.x.com",
"createdAt": "2026-02-06T21:09:45Z",
"lang": "en",
"author": { "id": "2244994945", "username": "XDevelopers", "name": "Developers", "...": "..." },
"counts": { "likes": 5599, "retweets": 896, "replies": 791, "quotes": 596, "bookmarks": 2803, "views": 2552260 },
"media": [],
"urls": ["http://developer.x.com"],
"hashtags": [],
"mentions": [],
"replyTo": null,
"quoted": null,
"retweetOf": null,
"conversationId": "2019881223666233717",
"isPinned": true,
"source": null,
"article": null
}
],
"nextCursor": "DAAHCgABHUS5pXT__-wLAAIAAAATMjEwMjQ0NzU2NzA0MDgyMzc2MAgAAwAAAAIAAA",
"previousCursor": "DAAHCgABHUS5pXUAJxELAAIAAAATMjEwODk2ODM2MDY5MjQwNDI2MQgAAwAAAAEAAA",
"hasNextPage": true
}- Same price, same parameters, same errors. Only the body of a successful (200) response changes.
- The response carries the header
X-Response-Format: simple, so you can tell which format you got. - Every reference page has a Raw / Simple toggle on its example response.
- Account actions (
/client/*) support it too: instead of the{"status_code", "response"}wrapper you get the new tweet, the user, the media ID, or{"data": {"success": true}}for actions such as like or bookmark. When X reports an error inside the wrapper, the wrapper is returned unchanged. Direct messages (/client/dm/*) return conversations, messages and settings in this format./premium/*ignores the option.
Envelope
Lists and timelines:
{ "data": [ ... ], "nextCursor": "…" | null, "previousCursor": "…" | null, "hasNextPage": true | false }Single objects:
{ "data": { ... } }data is null when X returns nothing for the ID or handle (for example GET /user/by/username/{username} for a handle that does not exist).
Pagination
Pass nextCursor as cursor to get the next page. nextCursor is null, and hasNextPage is false, when there is nothing more to fetch:
- the page has no items,
- X returns the same cursor you sent,
- X's end marker (follower and following lists answer
0|…on the last page).
So this loop always ends:
import requests
cursor, tweets = None, []
while True:
params = {"count": 20, "format": "simple", **({"cursor": cursor} if cursor else {})}
page = requests.get("https://api.twittrapi.com/user/12/tweets", params=params,
headers={"X-API-KEY": "YOUR_API_KEY"}).json()
tweets += page["data"]
if not page["hasNextPage"]:
break
cursor = page["nextCursor"]previousCursor is X's top cursor: use it to poll for newer items.
Rules
- Field names are camelCase and always present. A value X did not send is
null; a list X did not send is[]. Counts X does not send are0. - IDs are strings. Times are ISO 8601 in UTC (
2026-02-06T21:09:45Z). - Promoted tweets are dropped. Conversation threads (modules) are flattened in display order. A tweet that appears twice on a page is listed once.
- Tweets X does not return (deleted, suspended author, withheld) appear as
{"id": "…", "unavailable": true}when their ID is known, and are left out otherwise. The same placeholder is used forquoted. - Error responses (any status other than 200) are unchanged, see Errors. A 200 response that only holds X's
errors(nodata) is also returned raw, so you see X's message. - New fields may be added. Existing fields are not renamed or removed.
Tweet
| Field | Type | Notes |
|---|---|---|
id | string | |
url | string | https://x.com/<username>/status/<id> |
text | string | Full text: the long version of long posts (note tweets), t.co links replaced by their targets, the trailing media link removed, & < > decoded. Retweets: RT @user: plus the full original text. |
createdAt | string | null | ISO 8601 |
lang | string | null | X's language code (en, und, …) |
author | User | null | |
counts | object | likes, retweets, replies, quotes, bookmarks, views (views is null when X hides it) |
media | Media[] | |
urls | string[] | Expanded URLs in the text |
hashtags | string[] | Without # |
mentions | {id, username}[] | |
replyTo | object | null | {tweetId, userId, username} when the tweet is a reply |
quoted | Tweet | {id, unavailable} | null | One level: the quoted tweet's own quoted is null |
retweetOf | Tweet | null | The original tweet of a retweet (with its quoted) |
conversationId | string | null | ID of the first tweet of the thread |
isPinned | boolean | true for the pinned tweet at the top of a profile timeline |
source | string | null | App name, when X sends it (Twitter Web App) |
article | object | null | X Article: {id, title, previewText, coverImageUrl, publishedAt, text} |
Media
| Field | Type | Notes |
|---|---|---|
type | string | photo, video or gif |
url | string | Photos: the image. Videos and GIFs: the MP4 with the highest bitrate. |
previewUrl | string | The image (poster frame for videos) |
width, height | number | Original size |
altText | string | null | |
durationMs | number | null | Videos only |
User
| Field | Type | Notes |
|---|---|---|
id | string | |
username | string | Handle without @ |
name | string | Display name |
description | string | Bio, t.co links expanded |
location | string | As written by the user |
url | string | null | Website, expanded |
createdAt | string | null | ISO 8601 |
avatarUrl | string | null | Full size (not the 48 px _normal version) |
bannerUrl | string | null | |
verified | boolean | Blue check |
verifiedType | string | null | e.g. Business or Government; null for none |
protected | boolean | Private account |
counts | object | followers, following, tweets, likes, media, listed |
pinnedTweetIds | string[] | |
isAutomated | boolean | Account carries X's "Automated" label |
GET /user/about/{username} adds about: accountBasedIn, locationAccurate, createdCountryAccurate, source, usernameChanges, lastUsernameChangeAt, affiliateUsername, identityVerified, verifiedSince.
List
| Field | Type | Notes |
|---|---|---|
id, name, description | string | |
createdAt | string | null | ISO 8601 |
memberCount, followerCount | number | |
private | boolean | |
bannerUrl | string | null | |
owner | {id, username} | null |
Community
| Field | Type | Notes |
|---|---|---|
id, name, description | string | description is empty in search results |
createdAt | string | null | ISO 8601 |
memberCount, moderatorCount | number | |
joinPolicy | string | null | As X sends it, e.g. Open |
nsfw | boolean | |
topic | string | null | |
bannerUrl | string | null | |
creator | {id, username} | null | |
rules | {id, name, description}[] |
Job
| Field | Type | Notes |
|---|---|---|
id, title | string | |
location, url, description | string | null | url is the employer's application page |
company | {id, name, logoUrl} | null | |
postedBy | {id, username} | null |
GET /jobs/find-location/{term} returns {id, name} items to use as the job search location.
Trends
GET /trends/{locationId}: items {name, query, url, tweetVolume}; query is URL-decoded (#GoLang), tweetVolume is null when X has no count. GET /misc/locations: items {woeid, name, country, countryCode, placeType, parentId}.
Endpoints
| Endpoint | data |
|---|---|
/search (and its aliases), /tweet/{tweetId}, /tweet/{tweetId}/article, /tweet/{tweetId}/quotes, /user/{userId}/tweets, /tweetsAndReplies, /media, /likes, /articles, /communities/{communityId}/tweets, /lists/tweets | Tweet[] (search with category=People: User[]) |
/simpletweet/{tweetId} | Tweet |
/tweet/{tweetId}/favorites, /retweets, /user/{userId}/followers, /followings, /verifiedFollowers, /user/users/by/ids, /communities/{communityId}/members, /members/search, /moderators, /lists/members, /lists/followers | User[] |
/user/by/username/{username}, /user/{userId} | User |
/user/about/{username} | User with about |
/user/friendships/show | {source, target, following, followedBy, canDm, blocking, blockedBy, muting} |
/communities/search/{searchTerm} | Community[] |
/communities/{communityId} | Community |
/lists/details | List |
/jobs/search/{searchTerm} | Job[] |
/jobs/{jobId} | Job |
/jobs/find-location/{term} | {id, name}[] |
/trends/{locationId} | Trend[] |
/misc/locations | TrendLocation[] |
/username/to/id/{username}, /user/usernames/to/ids, /user/usernames/status, /misc/emojis | The usual response, unchanged, inside {"data": ...}. These are already small (or, for hashflags, a flat list), so there is nothing to simplify. |
POST /client/create, /client/reply | Tweet (a scheduled /client/create: {success: true, scheduledTweetId}) |
POST /client/home-timeline, /client/bookmarks | Tweet[] |
POST /client/schedule-overview | {id, text, executeAt, state, mediaIds}[] |
POST /client/follow, /client/profile, /client/profile/image, /client/profile/banner | User |
POST /client/media, /client/media/video | {mediaId, mediaKey, expiresAfterSecs} |
POST /client/communities/create | Community |
POST /client/retweet | {success: true, retweetId} |
POST /client/like, /client/deleteTweet, /client/bookmark, /client/unbookmark, /client/schedule-delete, /client/report, /client/communities/remove | {success: true} |
POST /client/dm/* | Conversations, messages and DM settings; the fields are on each endpoint page. |
Size and speed
Across the 9 captured endpoints the simple format is 64 % smaller in bytes (median, range 58–70 %) and uses 61 % fewer input tokens (median, range 52–68 %). Tokens shrink a little less than bytes because the fields that are dropped are mostly long, repetitive keys and IDs, which tokenize efficiently.
| Endpoint | Raw bytes | Simple bytes | Bytes saved | Raw tokens | Simple tokens | Tokens saved |
|---|---|---|---|---|---|---|
/user/{userId}/tweetsAndReplies | 81.7 KB | 24.2 KB | 70 % | 23,009 | 7,274 | 68 % |
/user/{userId}/tweets | 72.2 KB | 22.8 KB | 68 % | 20,484 | 6,866 | 66 % |
/tweet/{tweetId}/quotes | 51.7 KB | 21.1 KB | 59 % | 14,008 | 6,444 | 54 % |
/user/{userId}/media | 49.3 KB | 16.3 KB | 67 % | 14,883 | 4,919 | 67 % |
/communities/{communityId}/tweets | 40.4 KB | 14.5 KB | 64 % | 11,296 | 4,392 | 61 % |
/tweet/{tweetId} | 38.7 KB | 15.0 KB | 61 % | 10,601 | 4,669 | 56 % |
/search | 34.3 KB | 14.4 KB | 58 % | 9,582 | 4,556 | 52 % |
/user/{userId}/followers | 9.1 KB | 2.8 KB | 70 % | 2,272 | 801 | 65 % |
/user/{userId}/articles | 8.6 KB | 3.3 KB | 62 % | 2,263 | 1,009 | 55 % |
How this was measured (2026-10-11): one real captured response per endpoint (anonymized and shortened to a few items, so a full page with the default count is several times larger in both formats), converted with the same code the API runs. Both formats are minified JSON. Tokens are counted with OpenAI's o200k_base tokenizer (tiktoken, used by GPT-4o and later); with cl100k_base the median is 61 %. Claude and Gemini use their own tokenizers, so exact counts differ, but the reduction is of the same size because it comes from the fields that are left out.
The conversion runs on our side in about half a millisecond for a 300 KB timeline, so it does not add noticeable latency.
What the smaller responses mean in practice: context windows on Twitter/X data for AI agents, and storage for datasets on collecting X data for research.
When to use which
Use the simple format when you need the common fields and want code that does not break when X reshuffles its JSON. Use the raw format when you need something the simple format leaves out (cards, polls, edit history, community notes, X's ranking metadata): it has every field X sends.