How to migrate from the Twitter/X API
Migrating off the official X API takes five code changes: map each v2 endpoint to its new path, swap OAuth for an API key, drop fields and expansions, rewrite the JSON parsing and switch the pagination token.
Short answer: Reads are what to move: the official X API bills $0.005 per post and $0.010 per user, and from November 22, 2026 each author returned with a post adds $0.010. To move them, use Sorsa API: one ApiKey header, 20 tweets per search request from $0.07 per 1,000, and 100 free requests to test the map.
- Endpoint
POST /search-tweets- Page size
- 20 tweets per request
- Price
- from $0.07 per 1,000
#What changed on the official X API since 2023
Code written for the official X API mostly still runs; what changed since 2023 is what it may call, what it gets back and what each response costs. The dated list below comes from X's changelog (opens in a new tab) and its developer forum notices, with the part of an integration each change reaches.
| Date | Change on the official X API | What it does to your code |
|---|---|---|
| Mar 30, 2023 | Paid tiers replace free access; Free becomes write-only | Reads on a free app return errors |
| Jun 26, 2023 | Follows endpoints removed from Basic and Pro | Follower and following calls stop on those plans |
| Sep 28, 2023 | v1.1 search/tweets and users/search deprecated | v1.1 search code needs a new endpoint |
| Jan 19, 2026 | min_likes, min_replies, min_reposts dropped from search | Queries that used them had to drop them until May 4 |
| Feb 6, 2026 | Pay-per-use becomes the model; credits bought upfront | Calls are blocked once the balance hits zero |
| Feb 23, 2026 | Programmatic replies allowed only when the author summons you | Reply bots get refused |
| Apr 20, 2026 | Following, likes and quote-posts removed from self-serve | Those write calls stop working |
| May 4, 2026 | Search moves to a new index; retweets leave keyword results | The same query returns fewer posts |
| Jun 1 and Sep 1, 2026 | Legacy Basic, then Pro plans moved to pay-per-use | A monthly cap turns into a per-resource bill |
| Nov 22, 2026 | Expansions billed: $0.010 per author, $0.005 per media item | expansions=author_id can triple the cost of a post |
Mar 30, 2023
- Change on the official X API
- Paid tiers replace free access; Free becomes write-only
- What it does to your code
- Reads on a free app return errors
Jun 26, 2023
- Change on the official X API
- Follows endpoints removed from Basic and Pro
- What it does to your code
- Follower and following calls stop on those plans
Sep 28, 2023
- Change on the official X API
- v1.1
search/tweetsandusers/searchdeprecated - What it does to your code
- v1.1 search code needs a new endpoint
Jan 19, 2026
- Change on the official X API
min_likes,min_replies,min_repostsdropped from search- What it does to your code
- Queries that used them had to drop them until May 4
Feb 6, 2026
- Change on the official X API
- Pay-per-use becomes the model; credits bought upfront
- What it does to your code
- Calls are blocked once the balance hits zero
Feb 23, 2026
- Change on the official X API
- Programmatic replies allowed only when the author summons you
- What it does to your code
- Reply bots get refused
Apr 20, 2026
- Change on the official X API
- Following, likes and quote-posts removed from self-serve
- What it does to your code
- Those write calls stop working
May 4, 2026
- Change on the official X API
- Search moves to a new index; retweets leave keyword results
- What it does to your code
- The same query returns fewer posts
Jun 1 and Sep 1, 2026
- Change on the official X API
- Legacy Basic, then Pro plans moved to pay-per-use
- What it does to your code
- A monthly cap turns into a per-resource bill
Nov 22, 2026
- Change on the official X API
- Expansions billed: $0.010 per author, $0.005 per media item
- What it does to your code
expansions=author_idcan triple the cost of a post
Two of these changed results without changing a line of code. Since the new search index (opens in a new tab) went live on May 4, a keyword search that ran fine in April comes back without retweets. And from November 22, the same request with expansions=author_id that cost $5.00 per 1,000 posts costs up to $15.00, because X's expansions notice (opens in a new tab) bills each returned author as a user read.
The plans changed under running code too. X ended legacy Basic (opens in a new tab) subscriptions at their cutover dates after June 1, 2026, and legacy Pro (opens in a new tab) after September 1; a Pro monthly account was charged a $5,000 credit at cutover, with auto-recharge switched on. Every read now draws on credits at $0.005 per post and $0.010 per user or follower, up to 3M post reads a month, and the full rate card is in our X API pricing breakdown.
#What moves to Sorsa API and what stays on X
Every read of public data can move: search, timelines, post and user lookups, followers, lists, quotes, retweeters, replies, Spaces, communities and trends. Sorsa API, a Twitter (X) API and scraper in production since 2022 with 6B+ requests served, returns all of them through one key, with no OAuth and, as the guide to the Twitter API without a developer account explains, no X developer account. It is read-only by design, so the calls that write stay where they are.
Keep these on the official X API:
- Posting, replies, deletes and media upload.
- Direct messages, and likes, follows, bookmarks and blocks, as actions or as lists of your own account's activity.
- Who liked a post, and anything from protected accounts.
- Filtered stream, firehose, webhooks and the X Activity API: Sorsa API is polled, not pushed (the polling pattern is below).
- Post counts, Community Notes and the Ads API.
Your X account takes no part in the reads, because Sorsa API never asks for your login or cookies. After the split, the official X API bills only the writes, per request: $0.015 per post created and $0.20 for a post with a link.
The reads are where the money goes. On search and timeline reads, 1M tweets cost $5,000 on the official X API before any author billing, and from $70 on Sorsa API, which returns each tweet's author inside it at no extra cost. Lookups by ID go further: POST /tweet-info-bulk returns 100 tweets per request, from $0.014 per 1,000.
Official X API
$5,000
for 1M tweets
$0.005 per post, before authors
Sorsa API
from$70
for 1M tweets
from $0.07 per 1,000, author included
#Migrate to Sorsa API in six steps
Six steps take a v2 integration from inventory to cutover. The first five are edits to your code; the last one runs for a few days while both APIs answer the same reads.
-
List every call your code makes. Search the code for
api.x.com/2,api.twitter.com/2and your client's method names, such as Tweepy'ssearch_recent_tweetsor xdk'sposts.search_recent. Export last month's usage by resource in X's Developer Console, and mark each call as a read or a write. -
Get a key and send one request. Sign up, copy the key from the dashboard and call the endpoint that replaces your busiest read.
Terminal curl -X POST "https://api.sorsa.io/v3/search-tweets" \ -H "ApiKey: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query": "from:mira_moves_reads", "order": "latest"}'Response200 OK { "tweets": [ { "id": "1852331946812457001", "full_text": "Read path moved. Writes stay where they were.", "created_at": "2026-10-30T14:05:12Z", "lang": "en", "likes_count": 41, "retweet_count": 6, "reply_count": 3, "quote_count": 1, "view_count": 2380, "conversation_id_str": "1852331946812457001", "is_reply": false, "user": { "id": "1592041833471827968", "username": "mira_moves_reads", "display_name": "Mira Moves Reads", "followers_count": 912, "verified": false } } ], "next_cursor": "DAADDAABCgABGbHk7m4WYAEKAAIZsd" } -
Map each read to its endpoint. The table and the converter in the next section pair every v2 read with its Sorsa API path and parameters.
-
Swap OAuth, fields and expansions for one header. On read calls, delete the bearer token or the OAuth 1.0a signing and send
ApiKey: YOUR_API_KEYinstead. Droptweet.fields,user.fieldsandexpansions: every field and the author come back by default. -
Rewrite parsing and pagination. Read
tweetsoruserswhere you readdataandincludes, rename the fields that differ, and sendnext_cursorback until it comes back empty. -
Shadow-run, then cut over. Send a copy of your reads to Sorsa API for a few days, compare the results, move traffic in stages and keep the X key for writes.
#Map each X API v2 endpoint to Sorsa API
Every public read an integration is likely to call has one Sorsa API endpoint. The table pairs each v2 path, as written in X's OpenAPI specification (opens in a new tab), with its replacement and with how much one request returns; Sorsa API bills per request, not per item, so the last column sets the price.
| X API v2 call | Sorsa API endpoint | One request returns |
|---|---|---|
GET /2/tweets/search/recent | POST /search-tweets (opens in a new tab) | about 20 tweets |
GET /2/tweets/search/all | POST /search-tweets with since: and until: | about 20 tweets |
GET /2/tweets/:id | POST /tweet-info (opens in a new tab) | 1 tweet |
GET /2/tweets (by IDs) | POST /tweet-info-bulk (opens in a new tab) | up to 100 tweets |
GET /2/users/:id/tweets | POST /user-tweets (opens in a new tab) | about 20 tweets |
GET /2/users/:id/mentions | POST /mentions (opens in a new tab) | about 20 tweets |
GET /2/tweets/:id/quote_tweets | POST /quotes (opens in a new tab) | about 20 tweets |
GET /2/tweets/:id/retweeted_by | POST /retweeters (opens in a new tab) | a page of profiles |
Search with conversation_id: | POST /comments (opens in a new tab) | about 20 replies |
GET /2/users/by/username/:username | GET /info (opens in a new tab) | 1 profile |
GET /2/users and GET /2/users/by | GET /info-batch (opens in a new tab) | up to 100 profiles |
GET /2/users/:id/followers | GET /followers (opens in a new tab) | up to 200 profiles |
GET /2/users/:id/following | GET /follows (opens in a new tab) | up to 200 profiles |
GET /2/users/search | POST /search-users (opens in a new tab) | a page of profiles |
GET /2/lists/:id/members | GET /list-members (opens in a new tab) | a page of profiles |
GET /2/lists/:id/followers | GET /list-followers (opens in a new tab) | a page of profiles |
GET /2/lists/:id/tweets | GET /list-tweets (opens in a new tab) | about 20 tweets |
GET /2/spaces/:id | GET /spaces (opens in a new tab) | 1 Space |
GET /2/communities/:id | POST /community-info (opens in a new tab) | 1 community |
GET /2/communities/search | POST /search-communities (opens in a new tab) | a page of communities |
GET /2/trends/by/woeid/:woeid | GET /trends (opens in a new tab) | 1 location's trends |
GET /2/usage/tweets | GET /key-usage-info (opens in a new tab) | your balance, free of charge |
GET /2/tweets/search/recent- Sorsa API endpoint
POST /search-tweets(opens in a new tab)- One request returns
- about 20 tweets
GET /2/tweets/search/all- Sorsa API endpoint
POST /search-tweetswithsince:anduntil:- One request returns
- about 20 tweets
GET /2/tweets/:id- Sorsa API endpoint
POST /tweet-info(opens in a new tab)- One request returns
- 1 tweet
GET /2/tweets(by IDs)- Sorsa API endpoint
POST /tweet-info-bulk(opens in a new tab)- One request returns
- up to 100 tweets
GET /2/users/:id/tweets- Sorsa API endpoint
POST /user-tweets(opens in a new tab)- One request returns
- about 20 tweets
GET /2/users/:id/mentions- Sorsa API endpoint
POST /mentions(opens in a new tab)- One request returns
- about 20 tweets
GET /2/tweets/:id/quote_tweets- Sorsa API endpoint
POST /quotes(opens in a new tab)- One request returns
- about 20 tweets
GET /2/tweets/:id/retweeted_by- Sorsa API endpoint
POST /retweeters(opens in a new tab)- One request returns
- a page of profiles
Search with
conversation_id:- Sorsa API endpoint
POST /comments(opens in a new tab)- One request returns
- about 20 replies
GET /2/users/by/username/:username- Sorsa API endpoint
GET /info(opens in a new tab)- One request returns
- 1 profile
GET /2/usersandGET /2/users/by- Sorsa API endpoint
GET /info-batch(opens in a new tab)- One request returns
- up to 100 profiles
GET /2/users/:id/followers- Sorsa API endpoint
GET /followers(opens in a new tab)- One request returns
- up to 200 profiles
GET /2/users/:id/following- Sorsa API endpoint
GET /follows(opens in a new tab)- One request returns
- up to 200 profiles
GET /2/users/search- Sorsa API endpoint
POST /search-users(opens in a new tab)- One request returns
- a page of profiles
GET /2/lists/:id/members- Sorsa API endpoint
GET /list-members(opens in a new tab)- One request returns
- a page of profiles
GET /2/lists/:id/followers- Sorsa API endpoint
GET /list-followers(opens in a new tab)- One request returns
- a page of profiles
GET /2/lists/:id/tweets- Sorsa API endpoint
GET /list-tweets(opens in a new tab)- One request returns
- about 20 tweets
GET /2/spaces/:id- Sorsa API endpoint
GET /spaces(opens in a new tab)- One request returns
- 1 Space
GET /2/communities/:id- Sorsa API endpoint
POST /community-info(opens in a new tab)- One request returns
- 1 community
GET /2/communities/search- Sorsa API endpoint
POST /search-communities(opens in a new tab)- One request returns
- a page of communities
GET /2/trends/by/woeid/:woeid- Sorsa API endpoint
GET /trends(opens in a new tab)- One request returns
- 1 location's trends
GET /2/usage/tweets- Sorsa API endpoint
GET /key-usage-info(opens in a new tab)- One request returns
- your balance, free of charge
Four habits change with the paths. Tweet endpoints take POST with a JSON body, while user, list and utility lookups stay GET. A username works wherever v2 wanted a numeric ID, so the extra lookup before each timeline call goes away. tweet_link takes a full post URL or a bare ID. And POST /user-tweets keeps paging past the newest 3,200 posts, where X's user timeline stops.
Some v2 reads have no counterpart, because Sorsa API returns only what anyone can see on a public profile: liking users, a user's likes and bookmarks, post counts, the home timeline and every stream. Sorsa API also answers questions v2 can't: POST /check-follow (opens in a new tab) tells in one request whether one account follows another, and GET /about (opens in a new tab) returns an account's country and username history.
The converter below turns one v2 call at a time into its Sorsa API request, with the fields that change and the price of 1,000 results on each side.
curl -G "https://api.x.com/2/tweets/search/recent" \
-H "Authorization: Bearer $X_BEARER_TOKEN" \
--data-urlencode "query=from:mira_moves_reads" \
-d "max_results=100" \
-d "expansions=author_id" \
-d "tweet.fields=created_at,public_metrics"
curl -X POST "https://api.sorsa.io/v3/search-tweets" \
-H "ApiKey: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "from:mira_moves_reads", "order": "latest"}'
- Sorsa API endpoint
POST /search-tweets- One request returns
- about 20 tweets
- Official X API, 1,000 results
- $5.00, plus up to $10.00 for authors
- Sorsa API, 1,000 results
- from $0.07
Pagination: next_token becomes next_cursor in the JSON body. The same query searches back to 2006, not 7 days, and every tweet carries its author.
Pick the v2 call your code makes to see the same request on Sorsa API and what 1,000 results cost on each.
If your code uses filtered stream or webhooks
Sorsa API has no stream and no webhook, so a filtered stream rule becomes a search query you poll. Send the rule as a POST /search-tweets query with "order": "latest", add since_id: with the newest ID you have stored, and follow next_cursor so a busy minute with more than 20 new posts loses none of them.
import os
import time
import requests
URL = "https://api.sorsa.io/v3/search-tweets"
HEADERS = {"ApiKey": os.environ.get("SORSA_API_KEY", "YOUR_API_KEY")}
RULE = '"open data" lang:en'
def new_tweets(since_id):
body = {"query": f"{RULE} since_id:{since_id}", "order": "latest"}
found = []
while True:
r = requests.post(URL, headers=HEADERS, json=body, timeout=30)
if r.status_code == 429:
time.sleep(1)
continue
r.raise_for_status()
page = r.json()
found += page.get("tweets", [])
if not page.get("next_cursor"):
return found
body["next_cursor"] = page["next_cursor"]
newest = "1852331946812457001"
while True:
tweets = new_tweets(newest)
for t in sorted(tweets, key=lambda t: int(t["id"])):
print(t["id"], t["user"]["username"], t["full_text"][:60])
if tweets:
newest = max((t["id"] for t in tweets), key=int)
time.sleep(15)
One key runs 20 requests per second on every plan, which is 1,200 single-page polls a minute shared across your rules. Scheduling, alerts and deduplication for monitoring jobs are covered in the guide to real-time Twitter monitoring.
#Rewrite the response parsing
Sorsa API sends each tweet as one object with its author inside, so the join between data and includes disappears and the work left is renaming fields.
The field names below come from X's data dictionary and Sorsa API's live reference, read on November 2, 2026. Most changes are flattening, where a metric leaves public_metrics and keeps its name, and a few are renames.
| X API v2 field | Sorsa API field |
|---|---|
data[] | tweets[], or users[] for lists of accounts |
text and note_tweet.text | full_text |
author_id with includes.users[] | user, the full profile |
public_metrics.like_count | likes_count |
public_metrics.impression_count | view_count |
public_metrics.retweet_count, reply_count, quote_count, bookmark_count | the same names, one level up |
conversation_id | conversation_id_str |
referenced_tweets with includes.tweets[] | quoted_status, retweeted_status, in_reply_to_tweet_id |
attachments.media_keys with includes.media[] | entities[] with type, link, preview |
name on a user | display_name |
public_metrics.following_count, tweet_count on a user | followings_count, tweets_count |
meta.next_token and pagination_token | next_cursor, in both directions |
errors[] with title and detail | {"message": "..."} |
data[]- Sorsa API field
tweets[], orusers[]for lists of accounts
textandnote_tweet.text- Sorsa API field
full_text
author_idwithincludes.users[]- Sorsa API field
user, the full profile
public_metrics.like_count- Sorsa API field
likes_count
public_metrics.impression_count- Sorsa API field
view_count
public_metrics.retweet_count,reply_count,quote_count,bookmark_count- Sorsa API field
- the same names, one level up
conversation_id- Sorsa API field
conversation_id_str
referenced_tweetswithincludes.tweets[]- Sorsa API field
quoted_status,retweeted_status,in_reply_to_tweet_id
attachments.media_keyswithincludes.media[]- Sorsa API field
entities[]withtype,link,preview
nameon a user- Sorsa API field
display_name
public_metrics.following_count,tweet_counton a user- Sorsa API field
followings_count,tweets_count
meta.next_tokenandpagination_token- Sorsa API field
next_cursor, in both directions
errors[]withtitleanddetail- Sorsa API field
{"message": "..."}
IDs stay strings and dates stay ISO 8601, so stored data keeps its shape. Pagination collapses to one name: send next_cursor as a query parameter on GET endpoints or in the JSON body on POST endpoints, and stop when it comes back empty. Rate limiting gets simpler as well. The 15-minute windows and x-rate-limit-* headers described in our guide to X API rate limits give way to one limit of 20 requests per second per key: a 429 means wait a second and retry, and a 403 means the key is missing or the month's requests are used up.
Good to know
Page sizes are not fixed. A search page holds up to 20 tweets and can hold fewer while more pages remain, so loop on
next_cursor, never on the number of items, or one short page ends the job early.
#Tweepy and xdk code, before and after
Tweepy and xdk are free libraries; the bill comes from the official X API behind them, so the move swaps the client, not the language. Here is the same search with authors, paged to the end, in Tweepy and then in plain Python on Sorsa API:
import tweepy
client = tweepy.Client(bearer_token="X_BEARER_TOKEN")
for page in tweepy.Paginator(
client.search_recent_tweets,
query="from:mira_moves_reads",
tweet_fields=["created_at", "public_metrics"],
expansions=["author_id"],
user_fields=["username", "name"],
max_results=100):
users = {u.id: u for u in page.includes.get("users", [])}
for t in page.data or []:
author = users[t.author_id]
print(t.id, author.username, t.public_metrics["like_count"])
The field lists, the includes join and the paginator leave the code; one header comes in. xdk code changes the same way: Client(bearer_token=...) and client.posts.search_recent(...) page through the same data and includes, so they give way to the loop above. The query also reaches further after the move, because POST /search-tweets searches back to 2006 on every plan, where v2's recent search covers the last 7 days.
Code built on the twitter-api-v2 package for Node.js moves the same way, to fetch and one header:
const URL = "https://api.sorsa.io/v3/search-tweets";
const headers = {
ApiKey: process.env.SORSA_API_KEY ?? "YOUR_API_KEY",
"Content-Type": "application/json",
};
const body = { query: "from:mira_moves_reads", order: "latest" };
do {
const res = await fetch(URL, {
method: "POST",
headers,
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const page = await res.json();
for (const t of page.tweets ?? []) {
console.log(t.id, t.user.username, t.likes_count);
}
body.next_cursor = page.next_cursor;
} while (body.next_cursor);
For Tweepy code that only reads, move the reads to Sorsa API: search returns 20 tweets per request from $0.07 per 1,000, profiles come 100 per request from $0.014 per 1,000, and no OAuth is needed. Retries, batching and a pacer for 20 requests per second are in the guide to the Twitter API in Python.
#Translate your search queries
POST /search-tweets takes X's Advanced Search syntax, the language of the search box on x.com, so keywords, quoted phrases, from:, to:, lang:, conversation_id: and hashtags carry over unchanged. What changes are v2's is: and has: operators and its time parameters, which move into the query:
| X API v2 | Sorsa API query |
|---|---|
start_time and end_time parameters | since:2026-10-01 and until:2026-11-01 |
since_id and until_id parameters | since_id:ID and max_id:ID (inclusive) |
sort_order=recency or relevancy | "order": "latest" or "popular" in the body |
-is:retweet | nothing: retweets stay out by default |
is:reply and -is:reply | filter:replies and -filter:replies |
is:quote | filter:quote |
has:media, has:images, has:links | filter:media, filter:images, filter:links |
has:video_link | filter:native_video |
min_likes:, min_reposts:, min_replies: | min_faves:, min_retweets:, min_replies: |
point_radius: | geocode: |
start_timeandend_timeparameters- Sorsa API query
since:2026-10-01anduntil:2026-11-01
since_idanduntil_idparameters- Sorsa API query
since_id:IDandmax_id:ID(inclusive)
sort_order=recencyorrelevancy- Sorsa API query
"order": "latest"or"popular"in the body
-is:retweet- Sorsa API query
- nothing: retweets stay out by default
is:replyand-is:reply- Sorsa API query
filter:repliesand-filter:replies
is:quote- Sorsa API query
filter:quote
has:media,has:images,has:links- Sorsa API query
filter:media,filter:images,filter:links
has:video_link- Sorsa API query
filter:native_video
min_likes:,min_reposts:,min_replies:- Sorsa API query
min_faves:,min_retweets:,min_replies:
point_radius:- Sorsa API query
geocode:
Three details trip up translated queries. Native retweets are left out unless you add include:nativeretweets. geocode: takes latitude first, as in geocode:37.77,-122.41,5km, where v2's point_radius: took longitude first. And max_id: includes the ID itself, while v2's until_id excludes it. The full operator list, with recipes, is in our guide to Twitter search operators.
#Test with a shadow run, then cut over
A shadow run sends a copy of each read to Sorsa API while the official X API keeps serving your app, so the results are compared before anything depends on them.
The script runs one query over the same closed day on both APIs and compares the post IDs. Run it for each query and timeline you depend on; a closed window keeps new posts from skewing the count.
import os
import requests
X_URL = "https://api.x.com/2/tweets/search/recent"
X_HEADERS = {"Authorization": "Bearer " + os.environ["X_BEARER_TOKEN"]}
S_URL = "https://api.sorsa.io/v3/search-tweets"
S_HEADERS = {"ApiKey": os.environ.get("SORSA_API_KEY", "YOUR_API_KEY")}
def x_ids(query, start, end):
params = {"query": query, "start_time": start,
"end_time": end, "max_results": 100}
ids = set()
while True:
r = requests.get(X_URL, headers=X_HEADERS, params=params, timeout=30)
r.raise_for_status()
page = r.json()
ids |= {t["id"] for t in page.get("data", [])}
token = page.get("meta", {}).get("next_token")
if not token:
return ids
params["next_token"] = token
def sorsa_ids(query, since, until):
body = {"query": f"{query} since:{since} until:{until}",
"order": "latest"}
ids = set()
while True:
r = requests.post(S_URL, headers=S_HEADERS, json=body, timeout=30)
r.raise_for_status()
page = r.json()
ids |= {t["id"] for t in page.get("tweets", [])}
if not page.get("next_cursor"):
return ids
body["next_cursor"] = page["next_cursor"]
query = "from:mira_moves_reads"
a = x_ids(query, "2026-10-30T00:00:00Z", "2026-10-31T00:00:00Z")
b = sorsa_ids(query, "2026-10-30_00:00:00_UTC", "2026-10-31_00:00:00_UTC")
print(f"official X API {len(a)}, Sorsa API {len(b)}, shared {len(a & b)}")
print("only on X:", sorted(a - b)[:10])
print("only on Sorsa API:", sorted(b - a)[:10])
Expect small gaps rather than zero: posts deleted between the two calls drop out, and X's keyword search has left out retweets since May 4, 2026. X's recent search reaches back 7 days, so keep the window inside the last week. Once the IDs line up for the reads you run most, cut over in stages:
- Put both clients behind one interface and one config flag, so any read type can move back with a single value.
- Move 10% of reads, watch errors and
GET /key-usage-info, which is free and never counts against your requests, then move the rest. - Keep the official X API key for writes only, and lower its spending limit and auto-recharge amount in the Developer Console.
- Delete the read code paths, the field lists and the old pagination once a full billing cycle has passed.
#Move your reads off the official X API
A read-only v2 integration moves to Sorsa API, the Twitter API built for reading public X data, with one header, one endpoint per call and a parser that loses its join. Search runs from $0.07 per 1,000 tweets instead of $5.00, followers from $0.007 per 1,000 instead of $10.00, and every plan answers 20 requests per second. Start with your most expensive read, shadow-run it for a few days, and keep the official X API only for what writes.
#FAQ
Which Twitter API should I migrate to from the official X API?
For reading public X data, migrate to Sorsa API: each v2 read maps to one endpoint, tweets cost from $0.014 per 1,000 by ID and from $0.07 by search against $5.00 on the official X API, every plan runs at 20 requests per second, and search reaches back to 2006. One API key replaces OAuth. Sorsa API is read-only, so keep the official X API for posting.
How long does a migration off the official X API take?
The code change grows with the number of distinct v2 calls, not with your data volume: each call becomes one endpoint, one header and a few renamed fields. Add a shadow run of a few days in which both APIs answer the same reads and you compare the IDs. Posting, DMs and other writes stay on the official X API, so only the read path moves.
Can I keep posting to X after moving reads to Sorsa API?
Yes. Keep the official X API for posting, replies and DMs, and send reads to Sorsa API. Sorsa API is read-only by design: its key can't act on any X account and never needs your X login, so the two sit side by side without conflict. If your write path still signs with OAuth 1.0a user tokens, X has let apps exchange them for OAuth 2.0 tokens since September 21, 2026.
Do I need to migrate from v1.1 to v2 first?
No. Code that still calls v1.1 reads can map straight to Sorsa API: statuses/user_timeline becomes POST /user-tweets, statuses/show becomes POST /tweet-info, and users/show becomes GET /info. X lists v1.1 as legacy with limited support and deprecated its v1.1 search in 2023, so a stop on v2 means rewriting for OAuth 2.0 and pay-per-use billing before you move again.
What happened to legacy Basic and Pro X API plans?
X moved them to pay-per-use: legacy Basic subscriptions ended at their cutover dates after June 1, 2026, and Pro after September 1, 2026. A Pro monthly account was charged a $5,000 credit at cutover, with auto-recharge on. Reads now bill per resource, $0.005 per post and $0.010 per user, while the same tweets cost from $0.07 per 1,000 by search on Sorsa API.
#How we checked
On November 2, 2026 we built the endpoint map by comparing X's own OpenAPI specification (version 2.173, 200 paths) with Sorsa API's live reference (45 endpoints), and checked every v2 operator against X's operator list and every replacement against Sorsa API's search operators. The same day we read X's changelog, pricing page, pagination and rate-limit docs, the Python XDK reference and Tweepy 4.17.0's client, plus X's forum notices on the search index, legacy plans and expansion billing. We had no Sorsa API key for this pass, so the response shown uses demo values; we ran the Python and Node.js blocks against a local mock that answers in both APIs' documented shapes.
Sources
- X API changelog (docs.x.com) (opens in a new tab)
- X API pay-per-usage pricing (docs.x.com) (opens in a new tab)
- X API OpenAPI specification (opens in a new tab)
- Search operators, X API v2 (docs.x.com) (opens in a new tab)
- Search endpoints migrating to a new index (X Developers, May 2026) (opens in a new tab)
- Legacy Basic plans moving to pay-per-use (X Developers, May 21, 2026) (opens in a new tab)
- Legacy Pro plans moving to pay-per-use (X Developers, August 14, 2026) (opens in a new tab)
- X API PPU: expansions update (X Developers, October 23, 2026) (opens in a new tab)
- Python XDK (docs.x.com) (opens in a new tab)
- Sorsa API reference, OpenAPI
- Migration from the official X API (Sorsa API docs) (opens in a new tab)
- Search operators (Sorsa API docs) (opens in a new tab)