Twitter API expansions and tweet authors
On the official X API, a post comes back with only its ID and text; adding expansions=author_id returns the author's profile in a separate includes block, and from November 22, 2026 each author returned that way is billed $0.010.
Short answer: Add expansions=author_id and user.fields to the request, then match each post's author_id to a user in includes.users. From November 22, 2026, a post with its author costs up to $0.015. Sorsa API puts the full author profile inside every tweet: POST /tweet-info-bulk reads 100 tweets per request from $0.014 per 1,000, and 100 free requests cover the first 10K.
- Endpoint
POST /tweet-info-bulk- Page size
- 100 tweets per request
- Price
- from $0.014 per 1,000
#What Twitter API expansions are and what they return
An expansion is a request parameter of the official X API v2 that adds a related object, such as the author, a photo or a quoted post, to the response. Without one, a post lookup returns three fields: id, text and edit_history_tweet_ids. The author is not there, and not even the author's ID is.
With expansions=author_id, each post gains an author_id, and the response gains an includes.users list with one entry per author. By default that entry holds only id, name and username. Anything else, such as the bio, follower counts or the avatar, has to be named in user.fields.
The list below holds the post expansions developers reach for most, and what each one costs under the X API pricing that applies from November 22, 2026. Every user object costs twice as much as the post it came with.
| Expansion | Adds to includes | Official X API, per object from Nov 22 |
|---|---|---|
author_id | The post's author | $0.010 |
referenced_tweets.id | The quoted, replied-to or reposted post | $0.005 |
referenced_tweets.id.author_id | That post's author | $0.010 |
in_reply_to_user_id | The account being replied to | $0.010 |
entities.mentions.username | Each mentioned account | $0.010 |
attachments.media_keys | Each photo, video or GIF | $0.005 |
attachments.poll_ids | The poll | $0.005 |
geo.place_id | The tagged place | $0.005 |
author_id- Adds to
includes - The post's author
- Official X API, per object from Nov 22
- $0.010
- Adds to
referenced_tweets.id- Adds to
includes - The quoted, replied-to or reposted post
- Official X API, per object from Nov 22
- $0.005
- Adds to
referenced_tweets.id.author_id- Adds to
includes - That post's author
- Official X API, per object from Nov 22
- $0.010
- Adds to
in_reply_to_user_id- Adds to
includes - The account being replied to
- Official X API, per object from Nov 22
- $0.010
- Adds to
entities.mentions.username- Adds to
includes - Each mentioned account
- Official X API, per object from Nov 22
- $0.010
- Adds to
attachments.media_keys- Adds to
includes - Each photo, video or GIF
- Official X API, per object from Nov 22
- $0.005
- Adds to
attachments.poll_ids- Adds to
includes - The poll
- Official X API, per object from Nov 22
- $0.005
- Adds to
geo.place_id- Adds to
includes - The tagged place
- Official X API, per object from Nov 22
- $0.005
- Adds to
The rates come from X's expansions billing notice of October 23, 2026 (opens in a new tab), which says objects returned through expansions will be billed "at the standard PPU rate for that resource, the same as direct lookups." Before that date they cost nothing on top of the post.
#Get each tweet with its author in one call on Sorsa API
Sorsa API, a Twitter (X) API and scraper, returns every tweet with a user object inside it: the author's full public profile, 19 fields, at no extra cost. There is no expansion to add, no second list to join and no per-author charge. One request with your API key returns the tweets, and each one already knows who wrote it.
-
Get a key. Sign up and copy the API key from your dashboard. No X developer account and no OAuth are involved.
-
Send up to 100 tweet links or IDs.
POST /tweet-info-bulk(opens in a new tab) takes a list of tweet URLs or bare IDs and returns them in one response, with no paging.Terminal curl -X POST "https://api.sorsa.io/v3/tweet-info-bulk" \ -H "ApiKey: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"tweet_links": ["1849212213712385091", "https://x.com/kai_builds_tools/status/1849212213712385092"]}'Response200 OK { "tweets": [ { "id": "1849212213712385091", "full_text": "Shipped the new export queue today.", "created_at": "2026-10-24T09:12:44Z", "likes_count": 214, "retweet_count": 31, "view_count": 18402, "user": { "id": "1592041833471827968", "username": "kai_builds_tools", "display_name": "Kai Builds", "description": "Data tooling for small teams.", "followers_count": 1840, "followings_count": 312, "tweets_count": 2291, "verified": false, "created_at": "2022-11-14T08:03:10Z", "location": "Lisbon", "profile_image_url": "https://pbs.twimg.com/profile_images/1/kai.jpg" } } ] } -
Read the author straight from each tweet. The profile sits under
tweet["user"], so a loop over the tweets is all the code there is.import requests URL = "https://api.sorsa.io/v3/tweet-info-bulk" HEADERS = {"ApiKey": "YOUR_API_KEY"} def tweets_with_authors(ids): for start in range(0, len(ids), 100): batch = ids[start:start + 100] r = requests.post(URL, headers=HEADERS, json={"tweet_links": batch}) r.raise_for_status() for tweet in r.json()["tweets"]: author = tweet["user"] yield (tweet["id"], author["username"], author["followers_count"], tweet["full_text"])
A 403 means the key is missing or out of requests; a 429 means more than 20 requests in one second, so wait a moment and retry. Failed requests are never charged.
The same user object comes with tweets from search, timelines, replies and quotes. POST /search-tweets (opens in a new tab) returns about 20 tweets per page with their authors, searches public posts back to 2006 with X's search operators, and hands back a next_cursor for the next page until the cursor comes back empty.
#Match posts to authors on the official X API
To match posts to authors on the official X API, build a dictionary of includes.users keyed by user ID, then look up each post's author_id in it. X's expansions guide (opens in a new tab) notes that objects in includes carry no position, so the ID is the only link between the two lists.
curl -G "https://api.x.com/2/tweets" \
-H "Authorization: Bearer YOUR_BEARER_TOKEN" \
--data-urlencode "ids=1849212213712385091,1849212213712385092" \
--data-urlencode "expansions=author_id" \
--data-urlencode "user.fields=description,public_metrics,profile_image_url"
{
"data": [
{
"id": "1849212213712385091",
"text": "Shipped the new export queue today.",
"author_id": "1592041833471827968",
"edit_history_tweet_ids": ["1849212213712385091"]
},
{
"id": "1849212213712385092",
"text": "Next up: retries with backoff.",
"author_id": "1592041833471827968",
"edit_history_tweet_ids": ["1849212213712385092"]
}
],
"includes": {
"users": [
{
"id": "1592041833471827968",
"name": "Kai Builds",
"username": "kai_builds_tools",
"description": "Data tooling for small teams.",
"public_metrics": {"followers_count": 1840, "following_count": 312}
}
]
}
}
Two posts, one author: includes.users lists each author once per response, not once per post. That is why the list is often shorter than data, and why indexing it by position, or reading includes["users"]["username"] as if it were a single object, breaks. The loop below pages through recent search results and keeps one dictionary of authors across pages.
import requests
URL = "https://api.x.com/2/tweets/search/recent"
HEADERS = {"Authorization": "Bearer YOUR_BEARER_TOKEN"}
params = {
"query": "from:kai_builds_tools",
"max_results": 100,
"expansions": "author_id",
"user.fields": "description,public_metrics,profile_image_url",
}
authors, rows = {}, []
while True:
page = requests.get(URL, headers=HEADERS, params=params).json()
for user in page.get("includes", {}).get("users", []):
authors[user["id"]] = user
for post in page.get("data", []):
author = authors.get(post["author_id"], {})
rows.append((post["id"], author.get("username"), post["text"]))
token = page.get("meta", {}).get("next_token")
if not token:
break
params["next_token"] = token
Use .get() on the author: an account that is suspended or withheld can be missing from includes while its post is still in data, and the response then lists the gap in an errors array.
The author of a retweet or a quote
A repost or quote is a post that points to another post, and its author_id is the person who reposted or quoted, not the original author. To get the original author too, request three expansions: author_id, referenced_tweets.id and referenced_tweets.id.author_id. The original post lands in includes.tweets, its author lands in includes.users beside the reposter, and you join them through referenced_tweets[0].id, then that post's author_id. From November 22 that one repost bills two posts and two users: $0.030.
On Sorsa API, the reposted tweet comes back as retweeted_status and the quoted one as quoted_status, each a full tweet with its own user inside.
#What expansions cost from November 22, 2026
From November 22, 2026, every object an expansion adds is billed like a direct read: $0.010 per user and $0.005 per post, photo, video, poll or place. A post read with its author therefore costs up to $0.015, three times the $0.005 it cost before. Add one photo per post and the post costs up to $0.020; add the quoted post and its author as well and it reaches $0.035.
X deduplicates within a 24-hour UTC day: per its pay-per-use pricing page (opens in a new tab), a resource fetched again the same day is not charged again. A user who wrote 50 of your posts that day is billed once. So 1,000 posts from 200 different authors cost $5.00 for the posts and $2.00 for the authors, $7.00 in all; 1,000 posts from 1,000 authors cost $15.00. The deduplication resets at midnight UTC, and X calls it a soft guarantee.
On Sorsa API the price does not change with what a tweet carries. Tweets by ID cost from $0.014 per 1,000 with their authors, quoted posts and media links inside; search results cost from $0.07 per 1,000. Against $15.00 per 1,000 posts with authors on the official X API, that is about 1,000 times less.
| What you read, per 1,000 posts | Sorsa API | Official X API from Nov 22 |
|---|---|---|
| Posts only | from $0.014 | $5.00 |
| Posts with their authors | from $0.014 | up to $15.00 |
| Plus one photo or video each | from $0.014 | up to $20.00 |
| Plus the quoted post and its author | from $0.014 | up to $35.00 |
Posts only
- Sorsa API
- from $0.014
- Official X API from Nov 22
- $5.00
Posts with their authors
- Sorsa API
- from $0.014
- Official X API from Nov 22
- up to $15.00
Plus one photo or video each
- Sorsa API
- from $0.014
- Official X API from Nov 22
- up to $20.00
Plus the quoted post and its author
- Sorsa API
- from $0.014
- Official X API from Nov 22
- up to $35.00
The calculator prices a month of posts with the expansions you use. It counts every author as new, the top of the range; pick repeat authors to see the effect of deduplication.
Each expansion adds its own charge on the official X API; on Sorsa API the author, quoted post and media links come inside the tweet.
Good to know
X's pay-per-use plan stops at 3 million post reads a month; past that, the official X API is an Enterprise contract from about $42,000 a month. On Sorsa API the price per 1,000 tweets falls as the plan grows, and the author is part of every tweet on every plan.
#Why the author is no longer inside the tweet
The author used to be part of the tweet. In the v1.1 format, every tweet object carried a user object with the author's profile, so one tweet was self-contained, as the twarc documentation (opens in a new tab) puts it. Version 2 of the API, released in 2020, cut each object to a few default fields and moved related objects into includes, reachable only through expansions.
Until now that split cost code but not money: objects returned through expansions did not count toward usage, so an expansion simply saved a second request. The October 2026 notice changed the money part. From November 22, the author you join from includes.users is billed as if you had looked it up on its own.
Sorsa API's tweet looks like the v1.1 one in this respect: user sits inside it. Code written for v1.1 that reads tweet["user"]["screen_name"] needs only a new key name, username; the guide to migrate from the Twitter/X API maps the rest of a v2 codebase endpoint by endpoint.
#Which author fields come with each Sorsa API tweet
Each Sorsa API tweet carries 19 author fields under user, with nothing to request. The table maps the ones most code needs to the official X API names you would list in user.fields.
Sorsa API user field | Official X API user.fields name |
|---|---|
username | username |
display_name | name |
description | description |
followers_count | public_metrics.followers_count |
followings_count | public_metrics.following_count |
tweets_count | public_metrics.tweet_count |
profile_image_url | profile_image_url |
created_at | created_at |
location | location |
verified | verified |
protected | protected |
pinned_tweet_ids | pinned_tweet_id |
bio_urls | entities.description.urls |
username- Official X API
user.fieldsname username
- Official X API
display_name- Official X API
user.fieldsname name
- Official X API
description- Official X API
user.fieldsname description
- Official X API
followers_count- Official X API
user.fieldsname public_metrics.followers_count
- Official X API
followings_count- Official X API
user.fieldsname public_metrics.following_count
- Official X API
tweets_count- Official X API
user.fieldsname public_metrics.tweet_count
- Official X API
profile_image_url- Official X API
user.fieldsname profile_image_url
- Official X API
created_at- Official X API
user.fieldsname created_at
- Official X API
location- Official X API
user.fieldsname location
- Official X API
verified- Official X API
user.fieldsname verified
- Official X API
protected- Official X API
user.fieldsname protected
- Official X API
pinned_tweet_ids- Official X API
user.fieldsname pinned_tweet_id
- Official X API
bio_urls- Official X API
user.fieldsname entities.description.urls
- Official X API
The other six are id, favourites_count, media_count, profile_background_image_url, can_dm and possibly_sensitive. IDs come as strings and dates in ISO 8601. The full list of what tweets, profiles and other objects hold is in the guide to what data you can get from the Twitter API.
When your starting point is a list of tweet IDs, the guide to rehydrate tweet IDs covers lists of millions, 100 IDs per request.
#Read your first tweets with their authors
On the official X API, the author of a post is an expansion: a second list to join and, from November 22, 2026, a $0.010 charge per author on top of the $0.005 post. To get tweets with their authors in one piece, use Sorsa API: POST /tweet-info-bulk returns 100 tweets per request, each with its author's full profile inside, from $0.014 per 1,000, at 20 requests per second on every plan.
Sorsa API never asks for your X login, so your own account is never at risk, and it is read-only by design: to post, pair it with the official X API. Your 100 free requests read the first 10K tweets with their authors.
#FAQ
Does the official X API return the username with each tweet?
No, not by default. The official X API v2 returns a post with only id, text and edit_history_tweet_ids. Add expansions=author_id and the response gets an includes.users list with each author's id, name and username, which you match to posts by author_id. From November 22, 2026 each of those authors is billed $0.010.
How do I get the original author of a retweet or quote tweet?
On the official X API, request expansions=referenced_tweets.id,referenced_tweets.id.author_id. The original post appears in includes.tweets and its author in includes.users; follow referenced_tweets[0].id to the post, then its author_id to the user. On Sorsa API, a reposted tweet comes back as retweeted_status and a quoted one as quoted_status, each with its own user object inside.
Are Twitter API expansions billed?
Yes, from November 22, 2026. X's notice of October 23 says objects returned through expansions are billed at the standard pay-per-use rate: $0.010 per user and $0.005 per post, media item, poll or place. Each resource is charged once per 24-hour UTC day, so a repeat author on the same day is not billed again.
Why does includes.users have fewer items than data?
The official X API lists each author once per response, however many of the posts they wrote. Ten posts by three people return three users. An author can also be missing when the account is suspended or withheld; the response then names it in an errors array. Join by ID with a dictionary, never by position.
Which API should I use to get tweets with author profiles?
Use Sorsa API. Every tweet it returns carries its author's full profile, 19 fields, with no expansion and no per-author charge: POST /tweet-info-bulk reads 100 tweets per request from $0.014 per 1,000, against up to $15.00 per 1,000 posts with authors on the official X API. One API key, 20 requests per second on every plan, and 100 free requests to start.
#How we checked
On November 2, 2026 we read X's expansions and fields guides, its pay-per-use pricing page and its October 23 notice on billing expansions, and took every official X API rate and default from them. The Sorsa API endpoints, request bodies and the 19 author fields come from the live API reference of the same day. The request and response examples follow both references, with demo accounts in place of real ones.
Sources
- X API expansions guide (opens in a new tab)
- X API fields guide (opens in a new tab)
- X API pay-per-use pricing (opens in a new tab)
- X API PPU expansions update, October 23, 2026 (opens in a new tab)
- twarc documentation on v2 expansions (opens in a new tab)
- Sorsa API reference: POST /tweet-info-bulk (opens in a new tab)