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?

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 an ApiKey header.
  • Swap the base URL from https://api.x.com/2 to https://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, and expansions: every field returns by default.
  • Flatten parsers: remove the data, includes, and meta envelopes.
  • Rename fields: name to display_name, text to full_text, metrics move to the top level.
  • Replace pagination_token and next_token with next_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.

DimensionOfficial X API v2Sorsa API
AuthenticationOAuth 2.0 bearer (1.0a for user context)Single ApiKey header
Base URLhttps://api.x.com/2https://api.sorsa.io/v3
Field selectiontweet.fields, user.fields, expansionsAll fields returned by default
Response shapedata / includes / meta envelopesFlat objects, author inline
Paginationpagination_token / next_tokennext_cursor
HTTP method (tweets, search)GETPOST
BatchLimitedUp to 100 tweets or profiles per call
Rate limitsPer endpoint, 15-minute windowsFlat 20 requests per second, every plan
Billing unitPer resource fetchedPer request
Write actionsPosting, 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:

bash
curl -X GET "https://api.x.com/2/users/by/username/elonmusk" \
  -H "Authorization: Bearer AAAAAAAAAAAAAAAAAAAA..."

After:

bash
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:

ActionOfficial X API v2Sorsa API v3
User by usernameGET /2/users/by/username/:usernameGET /info?username=:username
User by IDGET /2/users/:idGET /info?user_id=:id
Multiple usersGET /2/users?ids=...GET /info-batch?usernames=...
FollowersGET /2/users/:id/followersGET /followers?user_id=:id
FollowingGET /2/users/:id/followingGET /follows?user_id=:id
Verified followersNot availableGET /verified-followers?user_id=:id
Account "about" metadataNot availableGET /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:

ActionOfficial X API v2Sorsa API v3
Single tweetGET /2/tweets/:idPOST /tweet-info
Multiple tweetsGET /2/tweets?ids=...POST /tweet-info-bulk
User timelineGET /2/users/:id/tweetsPOST /user-tweets
Quote tweetsGET /2/tweets/:id/quote_tweetsPOST /quotes
RetweetersGET /2/tweets/:id/retweeted_byPOST /retweeters
Replies (comments)No dedicated endpointPOST /comments
Long-form ArticleNot availablePOST /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:

ActionOfficial X API v2Sorsa API v3
Recent or full-archive searchGET /2/tweets/search/recentPOST /search-tweets
MentionsGET .../search/recent?query=@userPOST /mentions
Search usersNot availablePOST /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:

json
{
  "data": {
    "id": "44196397",
    "name": "Elon Musk",
    "username": "elonmusk",
    "public_metrics": {
      "followers_count": 100000000,
      "following_count": 500,
      "tweet_count": 30000
    }
  }
}

After:

json
{
  "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 v2Sorsa APINote
namedisplay_nameRenamed
textfull_textRenamed
public_metrics.followers_countfollowers_countFlattened
public_metrics.following_countfollowings_countFlattened, extra "s"
public_metrics.tweet_counttweets_countFlattened, renamed
public_metrics.like_countlikes_countFlattened, extra "s"
public_metrics.retweet_countretweet_countFlattened, no "s"
public_metrics.impression_countview_countFlattened, renamed
conversation_idconversation_id_strRenamed
in_reply_to_user_idin_reply_to_usernameHandle, not ID
author_id plus includes.users[]user (full object inline)Embedded
Referenced tweets via includesquoted_status, retweeted_statusInline

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:

bash
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:

json
{ "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.

ActionOfficial APISorsa API
Get a tweetGETPOST
Search tweetsGETPOST
User timelineGETPOST
Quote tweets, retweetersGETPOST
Replies (comments)n/aPOST
User profileGETGET
Followers, followingGETGET
ListsGETGET

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:

python
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:

python
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:

javascript
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:

javascript
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

python
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:

python
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:

  1. Replace Authorization: Bearer ... with the ApiKey header everywhere.
  2. Remove OAuth 1.0a signature logic (consumer keys, access tokens, signatures).
  3. Update the base URL to https://api.sorsa.io/v3.
  4. Remap every endpoint path using the Step 2 tables.
  5. Switch GET to POST for tweet, search, comment, quote, and retweeter endpoints.
  6. Delete tweet.fields, user.fields, and expansions.
  7. Remove the data / includes / meta unwrapping.
  8. Rename fields in your models (name, text, the public_metrics block).
  9. Replace pagination_token and next_token with next_cursor.
  10. Update error handling for the single message field shape.
  11. Set rate-limit logic to a flat 20 requests per second, no per-endpoint windows.
  12. Test critical endpoints in the API Playground before deploying.
  13. Track quota burn with the key-usage-info endpoint (see the key usage reference).
  14. 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.