Key Takeaway: Migrating from the official Twitter/X API to a third-party REST API involves four changes: replace OAuth with a single API key header, swap the base URL and remap endpoint paths, flatten response parsing, and switch pagination to one cursor field. A read-path migration usually takes one to three days.
By Sorsa Editorial · Updated June 13, 2026: rebuilt the endpoint mapping for the current Sorsa v3 surface of 40 endpoints, refreshed the official X API auth and pay-per-use details after the April 20 change, and rewrote the response-format field reference.
This is the technical walkthrough for teams that have already decided to leave and need the actual migration: authentication, endpoint mapping, response parsing, pagination, HTTP methods, and working code. We use Sorsa API, an alternative Twitter/X API we build and operate, as the migration target throughout, because the mapping from the official v2 endpoints is one of the cleanest available. Replacing OAuth with one ApiKey header, flat per-request billing where a single batch call returns up to 100 tweets or profiles, a flat 20 requests per second on every plan, and no developer-account approval to wait on are the practical reasons a read-path migration lands in days rather than weeks. If you are still choosing a provider, start with our comparison of Twitter/X API alternatives; this guide assumes that decision is made.
We have run this migration for teams since 2022, across more than 5 billion served requests, so the steps below follow a fixed checklist rather than a tour. The principles transfer to any read-only REST provider; the endpoint names, response fields, and code are Sorsa-specific.
Table of Contents
- What does migrating from the Twitter/X API involve?
- Step 1: Replace OAuth with an API key
- Step 2: Swap the base URL and remap endpoints
- Step 3: Flatten response parsing and remap fields
- Step 4: Switch pagination to a cursor
- Step 5: Switch GET to POST where required
- Step 6: Migrate the code
- Step 7: Keep your search queries and handle errors
- Migration checklist
- In practice: an academic group's archival migration
- Frequently asked questions
- How we verified this guide
- Getting started
What does migrating from the Twitter/X API involve?
Migrating from the official Twitter/X API to a flat-rate REST API is mostly subtraction: you remove OAuth, delete field-selection strings, drop the response envelopes, and remap a handful of endpoint paths. The core work touches authentication, endpoint paths, response parsing, and pagination, and a moderate read-path codebase moves in one to three days.
Here is the full changeset at a glance, before the step-by-step detail:
- Replace
Authorization: Bearer ...with anApiKeyheader. - Swap the base URL from
https://api.x.com/2tohttps://api.sorsa.io/v3. - Remap endpoint paths (tables in Step 2).
- Switch tweet and search endpoints from GET to POST (Step 5).
- Delete
tweet.fields,user.fields, andexpansions: every field returns by default. - Flatten parsers: remove the
data,includes, andmetaenvelopes. - Rename fields:
nametodisplay_name,texttofull_text, metrics move to the top level. - Replace
pagination_tokenandnext_tokenwithnext_cursor. - Simplify error handling: errors return as a single message field.
The table below maps the migration-relevant differences. It is the one comparison that matters here, because it decides how much of your code changes.
| Dimension | Official X API v2 | Sorsa API |
|---|---|---|
| Authentication | OAuth 2.0 bearer (1.0a for user context) | Single ApiKey header |
| Base URL | https://api.x.com/2 | https://api.sorsa.io/v3 |
| Field selection | tweet.fields, user.fields, expansions | All fields returned by default |
| Response shape | data / includes / meta envelopes | Flat objects, author inline |
| Pagination | pagination_token / next_token | next_cursor |
| HTTP method (tweets, search) | GET | POST |
| Batch | Limited | Up to 100 tweets or profiles per call |
| Rate limits | Per endpoint, 15-minute windows | Flat 20 requests per second, every plan |
| Billing unit | Per resource fetched | Per request |
| Write actions | Posting, DMs (follow, like, quote are Enterprise only) | Read-only |
Every line is a simplification except one: the official API can write to X, and a flat-rate read API cannot. If you post, send DMs, or run ads, that path stays on the official API. For reading public data, the swap removes OAuth, the field-selection layer, and the per-endpoint rate windows, which is most of the migration.
Step 1: Replace OAuth with an API key
Authentication is the change that removes the most code. The official API uses OAuth 2.0 bearer tokens for app-only requests and OAuth 1.0a (consumer keys, access tokens, per-request HMAC signatures) for anything needing a user context. A flat-rate provider replaces all of it with one key in a header.
Before, on the official API:
curl -X GET "https://api.x.com/2/users/by/username/elonmusk" \
-H "Authorization: Bearer AAAAAAAAAAAAAAAAAAAA..."
After:
curl -X GET "https://api.sorsa.io/v3/info?username=elonmusk" \
-H "ApiKey: YOUR_API_KEY"
There is no token refresh, no signature generation, and no callback URL. Generate a key once, store it in an environment variable, and every request carries the same ApiKey header. The full reference is in the authentication docs.
Step 2: Swap the base URL and remap endpoints
Each official v2 path maps to one new path. Most calls change only their URL and, for tweet endpoints, their HTTP method.
The base URL becomes https://api.sorsa.io/v3. Users map like this:
| Action | Official X API v2 | Sorsa API v3 |
|---|---|---|
| User by username | GET /2/users/by/username/:username | GET /info?username=:username |
| User by ID | GET /2/users/:id | GET /info?user_id=:id |
| Multiple users | GET /2/users?ids=... | GET /info-batch?usernames=... |
| Followers | GET /2/users/:id/followers | GET /followers?user_id=:id |
| Following | GET /2/users/:id/following | GET /follows?user_id=:id |
| Verified followers | Not available | GET /verified-followers?user_id=:id |
| Account "about" metadata | Not available | GET /about?username=:username |
GET /info-batch takes up to 100 usernames or IDs per call. GET /followers and GET /follows return up to 200 full profiles per page, where the official endpoint returns IDs you then rehydrate with a second call.
Tweets:
| Action | Official X API v2 | Sorsa API v3 |
|---|---|---|
| Single tweet | GET /2/tweets/:id | POST /tweet-info |
| Multiple tweets | GET /2/tweets?ids=... | POST /tweet-info-bulk |
| User timeline | GET /2/users/:id/tweets | POST /user-tweets |
| Quote tweets | GET /2/tweets/:id/quote_tweets | POST /quotes |
| Retweeters | GET /2/tweets/:id/retweeted_by | POST /retweeters |
| Replies (comments) | No dedicated endpoint | POST /comments |
| Long-form Article | Not available | POST /article |
POST /tweet-info-bulk returns up to 100 tweets in one request, where looping /tweet-info would cost 100. POST /user-tweets has no 3,200-tweet ceiling: paginate with next_cursor to the account's first post. The body field tweet_link accepts a full URL or just the numeric ID. For patterns that cut request counts, see the API usage optimization guide.
Search:
| Action | Official X API v2 | Sorsa API v3 |
|---|---|---|
| Recent or full-archive search | GET /2/tweets/search/recent | POST /search-tweets |
| Mentions | GET .../search/recent?query=@user | POST /mentions |
| Search users | Not available | POST /search-users |
POST /search-tweets covers historical search in the same endpoint, and POST /mentions adds engagement filters the official API does not expose: min_likes, min_replies, min_retweets, since_date, and until_date.
Lists, communities, verification, and analytics also map, and several have no official-API equivalent. Lists use GET /list-members, GET /list-followers, and GET /list-tweets. Communities, which the official API does not expose at all, use POST /community-members, POST /community-tweets, and POST /community-search-tweets. Single-call verification checks (POST /check-follow, GET /check-comment, POST /check-quoted, POST /check-retweet, POST /check-community-member) answer a yes/no question that would otherwise require crawling full follower or retweeter lists. The full path-by-path table lives in the endpoint mapping reference.
Step 3: Flatten response parsing and remap fields
This step touches the most code after authentication. The official API splits a response into data, includes, and meta. A flat-rate provider returns a flat object with the author embedded inside each tweet, so the user-join logic disappears.
A user profile, before:
{
"data": {
"id": "44196397",
"name": "Elon Musk",
"username": "elonmusk",
"public_metrics": {
"followers_count": 100000000,
"following_count": 500,
"tweet_count": 30000
}
}
}
After:
{
"id": "44196397",
"username": "elonmusk",
"display_name": "Elon Musk",
"followers_count": 100000000,
"followings_count": 500,
"tweets_count": 30000,
"verified": false,
"created_at": "2009-06-02T20:12:29Z"
}
The field renames are small but easy to miss in testing. Map them once and the rest follows:
| Official X API v2 | Sorsa API | Note |
|---|---|---|
name | display_name | Renamed |
text | full_text | Renamed |
public_metrics.followers_count | followers_count | Flattened |
public_metrics.following_count | followings_count | Flattened, extra "s" |
public_metrics.tweet_count | tweets_count | Flattened, renamed |
public_metrics.like_count | likes_count | Flattened, extra "s" |
public_metrics.retweet_count | retweet_count | Flattened, no "s" |
public_metrics.impression_count | view_count | Flattened, renamed |
conversation_id | conversation_id_str | Renamed |
in_reply_to_user_id | in_reply_to_username | Handle, not ID |
author_id plus includes.users[] | user (full object inline) | Embedded |
Referenced tweets via includes | quoted_status, retweeted_status | Inline |
One inconsistency to note so it does not cost you debugging time: likes and follows pluralize (likes_count, followings_count) while retweet_count, reply_count, and quote_count stay singular. The author profile lives under user in every tweet response, so the includes.users lookup table you maintained on the official API can be deleted outright.
Step 4: Switch pagination to a cursor
Pagination collapses to one field. The official API uses pagination_token in the query and returns meta.next_token; a flat-rate provider uses next_cursor at the top level of the response.
For GET endpoints, pass next_cursor as a query parameter. For POST endpoints, include it in the JSON body:
curl -X POST "https://api.sorsa.io/v3/search-tweets" \
-H "ApiKey: $API_KEY" \
-H "Content-Type: application/json" \
-d '{ "query": "from:elonmusk", "next_cursor": "ABC123" }'
The response is flat, and a missing or null next_cursor means you have reached the last page:
{ "tweets": [], "next_cursor": "XYZ789" }
The full pattern, including follower-list pagination, is in the cursor pagination docs.
Step 5: Switch GET to POST where required
This is the change teams forget and then debug for ten minutes. On the official API, tweet and search reads are GET. On a flat-rate REST API, anything that takes a tweet identifier or a search query becomes POST, while anything that takes a user identifier or a list ID stays GET.
| Action | Official API | Sorsa API |
|---|---|---|
| Get a tweet | GET | POST |
| Search tweets | GET | POST |
| User timeline | GET | POST |
| Quote tweets, retweeters | GET | POST |
| Replies (comments) | n/a | POST |
| User profile | GET | GET |
| Followers, following | GET | GET |
| Lists | GET | GET |
The rule of thumb: a tweet identifier or a search query means POST; a user identifier or a list ID means GET.
Step 6: Migrate the code
Three representative migrations follow, each in curl, Python, and JavaScript. These are the patterns you repeat across a read-path codebase.
Get a user profile
Before:
import requests
r = requests.get(
"https://api.x.com/2/users/by/username/elonmusk",
params={"user.fields": "public_metrics,verified,created_at"},
headers={"Authorization": f"Bearer {BEARER_TOKEN}"},
)
user = r.json()["data"]
followers = user["public_metrics"]["followers_count"]
name = user["name"]
After:
import requests
r = requests.get(
"https://api.sorsa.io/v3/info",
params={"username": "elonmusk"},
headers={"ApiKey": API_KEY},
)
user = r.json()
followers = user["followers_count"]
name = user["display_name"]
The field-selection string is gone and the metrics are top-level.
Search tweets
Before, the author join is mandatory:
const params = new URLSearchParams({
query: "from:elonmusk since:2024-01-01",
"tweet.fields": "created_at,public_metrics",
expansions: "author_id",
"user.fields": "username,name",
});
const res = await fetch(`https://api.x.com/2/tweets/search/recent?${params}`, {
headers: { Authorization: `Bearer ${BEARER_TOKEN}` },
});
const data = await res.json();
const users = Object.fromEntries((data.includes?.users || []).map(u => [u.id, u]));
for (const t of data.data || []) {
console.log(t.text, "by", users[t.author_id].username);
}
After, each tweet already carries its author:
const res = await fetch("https://api.sorsa.io/v3/search-tweets", {
method: "POST",
headers: { ApiKey: API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ query: "from:elonmusk since:2024-01-01" }),
});
const data = await res.json();
for (const t of data.tweets) {
console.log(t.full_text, "by", t.user.username);
}
The user-join table disappears because the author is embedded in every tweet.
Paginate every follower
def fetch_all_followers(user_id, api_key):
url = "https://api.sorsa.io/v3/followers"
headers = {"ApiKey": api_key}
followers, next_cursor = [], None
while True:
params = {"user_id": user_id}
if next_cursor:
params["next_cursor"] = next_cursor
data = requests.get(url, headers=headers, params=params).json()
followers.extend(data.get("users", []))
next_cursor = data.get("next_cursor")
if not next_cursor:
break
return followers
Each page returns up to 200 fully hydrated profiles, so a follower-graph pull that needed an IDs call plus a rehydrate call on the official API becomes a single pass. For language-specific detail, our Twitter API Python guide covers the full read path.
Step 7: Keep your search queries and handle errors
Your search queries transfer unchanged. A flat-rate provider that supports the same Twitter Advanced Search operators reads from:, to:, since:, until:, quoted phrases, hashtags, OR, and -is:retweet exactly as the official recent-search endpoint does, so existing query strings need no edits.
Copy your existing query strings across; the advanced search operators reference lists the full set. The mentions endpoint also exposes engagement filters (min_likes, min_replies, min_retweets, since_date, until_date), so any client-side "filter by engagement floor" logic you wrote against the official API can move server-side.
Error handling simplifies too. Where the official API returns a structured error array, a flat-rate provider returns a single message field with standard status codes: 400, 401, 403, 404, 429, and 500. For a 429, the policy is to wait one second and retry, because the limit is a flat 20 requests per second across every endpoint and plan, with no per-endpoint window to track. A defensive retry wrapper:
import time, requests
def call_with_retry(method, url, max_retries=3, **kwargs):
for attempt in range(max_retries):
r = requests.request(method, url, **kwargs)
if r.status_code == 429:
time.sleep(2 ** attempt)
continue
r.raise_for_status()
return r.json()
raise RuntimeError(f"failed after {max_retries} retries")
The full list of status codes is in the error codes reference.
Migration checklist
Use this as the working list for a read-path migration:
- Replace
Authorization: Bearer ...with theApiKeyheader everywhere. - Remove OAuth 1.0a signature logic (consumer keys, access tokens, signatures).
- Update the base URL to
https://api.sorsa.io/v3. - Remap every endpoint path using the Step 2 tables.
- Switch GET to POST for tweet, search, comment, quote, and retweeter endpoints.
- Delete
tweet.fields,user.fields, andexpansions. - Remove the
data/includes/metaunwrapping. - Rename fields in your models (
name,text, thepublic_metricsblock). - Replace
pagination_tokenandnext_tokenwithnext_cursor. - Update error handling for the single message field shape.
- Set rate-limit logic to a flat 20 requests per second, no per-endpoint windows.
- Test critical endpoints in the API Playground before deploying.
- Track quota burn with the
key-usage-infoendpoint (see the key usage reference). - Keep the official key if you also write to X; only the read path moves.
In practice: an academic group's archival migration
An academic research group came to us in mid-2025 running a tweet-collection pipeline for a longitudinal study on the official API. Two problems had stalled them: the user-timeline endpoint capped at the most recent 3,200 tweets per account, which broke their historical coverage, and the follower endpoint returned bare IDs that needed a second lookup to become usable profiles.
The read-path migration took about two days for one researcher. POST /user-tweets removed the 3,200-tweet ceiling, paginating with next_cursor back toward each account's first post, so the archival gap closed. The follower pull collapsed to a single pass because GET /followers returns up to 200 full profiles per page, which roughly halved the request count for that part of the job. The only real friction was the field renames (name to display_name, text to full_text, and the pluralized likes_count), caught in a few hours of testing rather than design. The exact numbers vary by workload; the shape of the migration does not.
Frequently asked questions
Can you migrate from the Twitter/X API one endpoint at a time?
Yes, and a gradual migration is usually the safer path. Put a thin abstraction layer in front of your data calls, point one endpoint at the new provider, validate its output against the official API for a few days, then move the next. Application code never has to change in one big rewrite, and you can roll an endpoint back independently if anything looks off.
How do you test a Twitter/X API migration without breaking production?
Run the two APIs in parallel and diff the parsed output before cutting over. The lightest option is the browser playground, which sends requests with no integration code. A stronger option is a side-by-side script that calls both APIs and compares results, and the most thorough is a feature flag that routes a percentage of traffic to the new provider so you can roll back instantly.
Do Twitter Advanced Search queries still work after migrating?
Yes, if the provider supports the same Advanced Search operators. Sorsa's search-tweets and mentions endpoints read from:, to:, since:, until:, quoted phrases, hashtags, OR, and -is:retweet exactly as the official recent-search endpoint does, so existing query strings transfer without edits. The mentions endpoint adds engagement filters, such as min_likes and min_retweets, that the official API does not expose.
How do you handle rate limits and errors after migrating?
Error handling gets simpler. Sorsa returns each error as a single message field rather than the official API's structured error array, with standard status codes (400, 401, 403, 404, 429, and 500). The rate limit is a flat 20 requests per second on every plan, with no per-endpoint 15-minute windows. On a 429, wait one second and retry; there is no reset header to track.
Does migrating remove the 3,200-tweet timeline limit?
Yes. The official v2 user-timeline endpoint caps at the most recent 3,200 tweets per account. Sorsa's POST /user-tweets has no such cap: paginate with next_cursor until the response stops returning one, and you reach the account's first post. For archival, sentiment-history, and training-data work, this is often the reason the migration happens at all.
How do you keep writing to X if the alternative is read-only?
You keep the official X API for write actions and migrate only the read path. Posting, replies, DMs, likes, follows, and ads all stay on the official API, which is the only system that can act on a user's behalf. Most teams end up with a hybrid: a small official-API budget for writes and a flat-rate read API for high-volume data pulls. Sorsa is read-only by design, which also removes a class of write-permission and account-suspension risk.
How we verified this guide
This walkthrough draws on our hands-on work operating an alternative X API since 2022 and on read-path migrations we have run for teams leaving the official platform. Endpoint paths, parameter names, response fields, and the ApiKey header were verified against the live Sorsa API documentation, and the official X API authentication model, response envelopes, and pay-per-use rates were checked against X's developer documentation and pricing page, reflecting the April 20, 2026 change. For the cost side of the decision, see our current X API pricing breakdown. Every code sample was written to run as shown. Verified June 13, 2026.
Reviewed by Keksich, founder of Sorsa, marketer and X API researcher.
Getting started
If your pipeline reads tweets, profiles, followers, or search results, the fastest way to scope a migration is to run a few calls and compare the response shape against your current parser. The Sorsa API quickstart gets you to a first request in minutes behind one key, and the historical data docs show how the timeline endpoints reach back to 2006 without the 3,200-tweet cap. Starter access is $49 a month for 10,000 requests, every plan holds the same flat 20 requests per second, and there is no developer-account approval between you and your first call. Point one endpoint at it behind a flag, diff the output, and migrate the rest.