By Sorsa Editorial

Published May 21, 2026. Updated September 23, 2026: rewritten around the 2026 X API changes, with the legacy Basic and Pro pay-per-use cutovers, the May search changes, an X API vs Sorsa comparison, a search-operator translation table, and a parallel-test script. Official X API pay-per-use rates verified September 2026.

Key Takeaway: In 2026 the X API switched to pay-per-use billing, began moving legacy Basic and Pro plans onto it, capped post reads at 3 million a month, dropped follow, like, and quote-post writes, and changed search. Read-heavy apps can keep writes on X and move reads to a cheaper flat per-request API within days.

The X API, still called the Twitter API in most codebases, changed on almost every axis developers depend on in 2026: how it bills, what self-serve apps can write, how search behaves, and which plans still exist. If an integration broke this year, or a bill suddenly looks different, one of these Twitter API changes is the reason.

For most teams the fix sits on the read side, where the bill grows fastest. Sorsa API, a read-only Twitter/X data API, returns public posts, profiles, and follower lists as flat JSON behind a single ApiKey header. Billing is flat: one call counts as one request whatever it returns, the author profile rides along in every post, and batch endpoints return up to 100 posts or 200 followers per call. On batch endpoints that works out to about $0.02 per 1,000 tweets on the Pro plan (from $0.049 on Starter), and 100 free requests, no card and no expiry, cover a full parallel test before any spend.

This guide does two jobs. The first half lists each major change with its date, the symptom it produces, and the fix. The second half is a migration playbook for the read path: an endpoint map, a field map, a search-operator translation table, a v2-compatible shim, and a parallel-test script. Together they let you move reads to a flat per-request API while writes stay on X.

Contents

What changed in the X API in 2026?

The eight X API updates that matter most for code and budgets, in order:

Date (2026)What changedWho it hits
Feb 6Pay-per-use launches: credits are bought upfront in the new Developer Console and every resource returned is billed. Basic and Pro stay available for the moment, and existing subscribers can opt in.New apps. Legacy Free users got a $10 voucher.
Feb 23Programmatic replies through POST /2/tweets need a "summons": the post's author must mention or quote your account.Reply bots, auto-responders
Apr 20Posting rises to $0.015 per post and $0.20 per post with a URL. Owned reads drop to $0.001. Follow, like, and quote-post endpoints leave every self-serve plan.Growth tools, schedulers, link-sharing bots
May 4Search endpoints move to a new index. New min_likes:, min_replies:, and min_reposts: operators arrive, and keyword searches stop returning retweets.Search dashboards, monitoring, analytics
May 30X shuts down Communities, after pushing back the original May 6 date.Anything that tracked community members or feeds
After Jun 1Legacy Basic plans, monthly and annual, move to pay-per-use at the end of each billing cycle.Former $200/month subscribers
After Sep 1Legacy Pro plans move to pay-per-use at each account's own cutover date (announced in August).Former $5,000/month subscribers
Sep 21Apps can exchange stored OAuth 1.0a tokens for OAuth 2.0 tokens without re-authorizing users.Apps still signing requests with OAuth 1.0a

Sources: the official X API changelog, X's pay-per-use pricing page, and the developer-forum announcements for the legacy Basic and legacy Pro cutovers.

One more rule shapes every budget: pay-per-use accounts are capped at 3 million post reads per billing cycle. Past the cap, post reads stop until the next cycle unless you sign an Enterprise contract.

The 2026 changes sit on top of the ones that started the paid era. Most free read access disappeared in 2023, when paid tiers took over, and in October 2024 the Basic plan doubled to $200 a month for 15,000 post reads. The background is in why the X API got so expensive, and every line item is in the full X API rate card for 2026.

If your legacy Basic or Pro plan just converted

  • Check the Developer Console first. Confirm the cutover date, the spending limit, and the auto-recharge rule. Converted monthly accounts receive default recharge settings that may not match your budget, and annual subscribers' prorated credits expire after the matching number of months.
  • Export a month of usage by resource type. Posts, users, and followers bill at different rates, so the mix decides the new bill.
  • Price reads and writes separately. Writes have to stay on X. Reads do not.
  • Watch the 3 million cap. It blocks post reads mid-cycle instead of billing overage, so a busy month can stop a pipeline.

What broke, and how do you fix it?

Most 2026 breakages trace back to one of these changes:

What you seeWhat changedWhat to do
Follow, like, or quote-post calls failRemoved from self-serve plans on April 20Drop the feature or negotiate Enterprise. No self-serve workaround exists.
Automated replies are rejectedSummons rule since February 23Reply only to posts that mention or quote your account
Keyword search stopped returning retweetsNew search index on May 4Read reposts of any post with a reposters endpoint (POST /retweeters on Sorsa), or query an account's recent retweets with from:username filter:nativeretweets
min_faves: or min_retweets: return a 400 errorThe v2 names are min_likes: and min_reposts:Rename them, or keep the web names unchanged on an Advanced Search API such as Sorsa's POST /search-tweets
Community members or feeds come back emptyX shut down Communities in May (TechCrunch)Track the same people with Lists or keyword search
Monthly cost changed after a plan convertedFlat Basic and Pro fees became per-resource billingSet a spending cap, cache results so the same posts are not re-read on later days (same-day re-reads are billed once), and move bulk reads to flat per-request billing
Post reads blocked before the month ends3 million post-read cap per billing cycleMove bulk reads to a flat per-request plan, where only the request quota counts, or negotiate Enterprise

Write-side problems have no fix outside X: a read-only API cannot follow, like, or post. Billing and search problems are different. They are exactly what a read-path migration solves, and for most data pipelines the read path is where the money goes.

Should you stay on pay-per-use or move your reads?

Pay-per-use is cheap at low volume and expensive at scale, because every post and every profile in a response is billed on its own. Flat per-request billing matches it at about 10,000 post reads a month and pulls ahead quickly after that, since each extra post, profile, or follower on a flat plan costs a fraction of a cent.

Your situationBest pathWhy
A few thousand posts a month and nothing elsePay-per-use is fine for nowA few dollars a month; revisit as volume grows
Over about 10,000 posts a month, or regular profile and follower pullsMove reads to a flat per-request APICosts match at 10,000 posts and diverge fast after that
You post, reply, or DM, and also read at volumeHybrid: writes on X, reads on a flat per-request APIThe X bill shrinks to writes only
Analytics, monitoring, research, or AI agents that only readMove all readsLargest savings, and you authenticate with one API key
Over 3 million post reads a monthMove readsPay-per-use blocks reads past the cap; flat plans only count requests
You need real-time push deliveryKeep filtered stream for that one jobEverything else can still move

A flat per-request read API such as Sorsa bills one request per call, whether the response carries 1 post or 200 profiles. With batch endpoints, reads start from $0.02 per 1,000 tweets and from $0.01 per 1,000 profiles, which makes read-heavy workloads up to 50x cheaper than per-resource billing. Sorsa is read-only by design: it takes over the read path, which in most data pipelines is where the bill sits, while posting stays on X. For the wider market, see Twitter API alternatives compared.

Official X API vs Sorsa API at a glance

Official X API (pay-per-use)Sorsa API
Billing unitEvery post, profile, or follower returnedEvery request, whatever it returns
1,000 posts$5.00from $0.02
1,000 follower profiles$10.00from $0.01
Author data on each postJoined from includes through expansionsFull profile embedded in every post
Monthly read ceiling3 million post reads per billing cycleYour plan's request quota; custom plans above 500,000 requests
Rate limitsPer endpoint, in 15-minute windows20 requests per second on every plan
AuthenticationOAuth 2.0 or OAuth 1.0aOne ApiKey header
User timeline depthLatest 3,200 postsNo 3,200-post ceiling
Search syntaxX API v2 operatorsX Advanced Search operators
Posting and DMsYes (follows, likes, quote-posts are Enterprise only)Read-only
What you need to startAn X developer account and prepaid creditsAn API key; 100 free requests, no card

What does your workload cost after the switch?

Pay-per-use charges $5.00 per 1,000 posts and $10.00 per 1,000 profiles or followers. Flat per-request billing charges per call instead: from $0.02 per 1,000 tweets through the bulk lookup endpoint, about $0.10 per 1,000 tweets through paginated search on the Pro plan, and from $0.01 per 1,000 profiles through follower lists.

Monthly read workloadLegacy flat planPay-per-use todayFlat per-request billing
15,000 posts (the legacy Basic limit)$200 (Basic)$75750 requests, Starter plan, $49
100,000 posts$5,000 (Pro)$5005,000 requests, Starter plan, $49
1,000,000 posts (the legacy Pro limit)$5,000 (Pro)$5,00050,000 requests, Pro plan, $199
3,000,000 posts (the pay-per-use cap)Enterprise only$15,000, then blocked150,000 requests, Enterprise plan, $899
1,000,000 follower profilesVaried by plan$10,0005,000 requests, Starter plan, $49

The flat-rate column assumes full pages: 20 posts per search or timeline request and 200 profiles per follower request. Some pages come back partly filled, so treat the request counts as a floor and leave headroom when picking a plan. The pay-per-use column ignores two discounts. Re-reading the same post within one UTC day is billed once, and once a billing cycle passes $200, X returns 10% to 20% of spend as xAI API credits, which only helps if you use xAI's API. Neither discount closes a gap of this size.

Worked example: re-pricing a legacy Pro read pipeline

Consider a brand-monitoring pipeline on legacy Pro. Each month it reads 800,000 posts through search, looks up 100,000 author profiles, and snapshots 500,000 follower records, and it publishes about 2,000 posts. On the flat Pro plan, all of that cost $5,000 a month.

After the Pro cutover, pay-per-use prices each resource:

  • 800,000 posts at $0.005: $4,000
  • 100,000 profiles at $0.010: $1,000
  • 500,000 follower records at $0.010: $5,000
  • 2,000 posts published at $0.015: $30 (or $400 if every post carries a link)

That is about $10,030 a month, double the old plan, and it uses 800,000 of the 3 million post-read cap.

Moving only the reads to Sorsa changes the math. Search at 20 posts per request is 40,000 requests, batch profile lookups at 100 per request add 1,000, and follower pages at 200 per request add 2,500. The total, 43,500 requests, fits inside the 100,000-request Pro plan at $199 a month. Publishing stays on X at $30, so the hybrid setup runs about $229 a month, with more than half of the plan's quota still free for growth. The code side is Steps 1 to 7 below: with the shim from Step 4 in place, existing parsers keep working on day one while the rest of the migration proceeds endpoint by endpoint.

Check your own numbers. Enter your monthly volumes in the X API cost calculator to see which plan fits, then get an API key and run your real queries: every new account includes 100 free requests, no card required.

How do you migrate the read path from the X API?

Seven changes cover a typical read-path migration, and most teams finish in one to three engineering days. The steps below include the parts teams usually get stuck on: search operators, a compatibility shim, and a safe cutover.

Step 1: Replace OAuth with one API key header

Authentication removes the most code. The official API uses OAuth 2.0 bearer tokens for app-only reads and OAuth 1.0a or OAuth 2.0 user tokens for user context. After the switch, your code sends one header.

bash
# Before: X API v2 with an app-only bearer token
curl "https://api.x.com/2/users/by/username/nasa?user.fields=public_metrics,created_at" \
  -H "Authorization: Bearer $X_BEARER_TOKEN"

# After: one API key header, all fields returned by default
curl "https://api.sorsa.io/v3/info?username=nasa" \
  -H "ApiKey: $SORSA_API_KEY"

You stop generating OAuth 1.0a signatures, refreshing tokens, and registering callback URLs. You also don't need an X developer account, as this guide to getting X data without a developer account explains. Create a key in the dashboard, store it in an environment variable, and send it as ApiKey on every request.

Step 2: Change the base URL and remap X API v2 endpoints

The base URL moves from https://api.x.com/2 to https://api.sorsa.io/v3. Each read endpoint has a direct counterpart. The Tweepy column helps if your code calls the library instead of raw URLs.

Data you readX API v2 endpointTweepy Client methodSorsa API v3
Profile by usernameGET /2/users/by/username/:usernameget_user(username=...)GET /info?username=
Profile by IDGET /2/users/:idget_user(id=...)GET /info?user_id=
Up to 100 profilesGET /2/users?ids= or /2/users/by?usernames=get_users()GET /info-batch?usernames=a&usernames=b
FollowersGET /2/users/:id/followersget_users_followers()GET /followers?user_id= (200 per page)
FollowingGET /2/users/:id/followingget_users_following()GET /follows?user_id= (200 per page)
User searchGET /2/users/search (user context)nonePOST /search-users
Single postGET /2/tweets/:idget_tweet()POST /tweet-info
Up to 100 postsGET /2/tweets?ids=get_tweets()POST /tweet-info-bulk
X Article textGET /2/tweets/:id with tweet.fields=articleget_tweet()POST /article
User timelineGET /2/users/:id/tweets (latest 3,200)get_users_tweets()POST /user-tweets (no 3,200 ceiling)
MentionsGET /2/users/:id/mentionsget_users_mentions()POST /mentions
Recent or full-archive searchGET /2/tweets/search/recent, /search/allsearch_recent_tweets(), search_all_tweets()POST /search-tweets
Replies to a postsearch with conversation_id:search_recent_tweets()POST /comments
Quote postsGET /2/tweets/:id/quote_tweetsget_quote_tweets()POST /quotes
RepostersGET /2/tweets/:id/retweeted_byget_retweeters()POST /retweeters
List membersGET /2/lists/:id/membersget_list_members()GET /list-members?list_id=
List followersGET /2/lists/:id/followersget_list_followers()GET /list-followers?list_link=
List postsGET /2/lists/:id/tweetsget_list_tweets()GET /list-tweets?list_id=
Space detailsGET /2/spaces/:idget_space()GET /spaces?id=
Trends by locationGET /2/trends/by/woeid/:woeidnoneGET /trends?woeid=

A few reads have no v2 counterpart and often replace custom code:

  • GET /verified-followers returns only verified followers of an account.
  • GET /about returns the "About this account" data: country, the number of username changes and the date of the last one, and the Premium start date.
  • POST /check-follow, POST /check-retweet, POST /check-quoted, and GET /check-comment answer yes/no questions that would otherwise mean crawling whole follower or reposter lists.

Long-form posts get a dedicated call too: POST /article returns the full text, cover image, and metrics in one flat object, as the X Articles API guide shows.

Follower pulls change the most in cost. On pay-per-use, 1,000 followers cost $10. On a flat plan, the same 1,000 profiles take 5 requests. The followers and following lists guide covers full-graph exports.

Step 3: Move post and search calls to POST

This is the change most often missed while debugging. On the official API every read is GET. Here, most endpoints that take a post or a search query use POST with a JSON body, while profile, follower, list, Space, and trend lookups stay GET. Treat the method column in the map above as the source of truth.

Two details save time. Fields named tweet_link accept either a full post URL or the bare numeric ID. And on GET endpoints, list parameters are passed by repeating the key (?usernames=a&usernames=b).

Step 4: Flatten response parsing, or add a v2 shim

The official API splits every response into data, includes, and meta, and you join authors back to posts through author_id. Here, each post is a flat object with the full author profile embedded under user, and metrics sit at the top level.

X API v2 fieldSorsa fieldChange
namedisplay_nameRenamed
textfull_textRenamed
public_metrics.followers_countfollowers_countFlattened
public_metrics.following_countfollowings_countFlattened, extra "s"
public_metrics.tweet_counttweets_countFlattened, renamed
public_metrics.listed_countnot returnedDropped
public_metrics.like_countlikes_countFlattened, extra "s"
public_metrics.retweet_count, reply_count, quote_count, bookmark_countsame namesFlattened
public_metrics.impression_countview_countRenamed
conversation_idconversation_id_strRenamed
in_reply_to_user_idin_reply_to_usernameHandle instead of ID
author_id plus includes.users[]user on every postEmbedded
referenced_tweets plus includes.tweets[]quoted_status, retweeted_statusEmbedded
entities (urls, mentions, hashtags) and mediaentities array of {type, link, preview} for photos, videos, and URLsMentions and hashtags come from full_text
meta.next_tokennext_cursorMoved to top level

Watch the plurals: likes_count, followings_count, and tweets_count take an "s", while retweet_count, reply_count, and quote_count do not. Profiles also carry extra fields such as favourites_count, media_count, can_dm, bio_urls, and pinned_tweet_ids, with no field-selection parameters needed.

There are two ways to absorb the new shape. Rewrite parsers to read the flat fields, or wrap calls in a shim that returns v2-shaped objects so the rest of the codebase keeps working. The shim is the faster first step, and you can retire it one endpoint at a time.

python
import os
import requests

SORSA = "https://api.sorsa.io/v3"
HEADERS = {"ApiKey": os.environ["SORSA_API_KEY"]}

def v2_user(u):
    """Reshape a flat profile into the X API v2 user object."""
    return {
        "id": u["id"],
        "username": u["username"],
        "name": u["display_name"],
        "description": u.get("description"),
        "verified": u.get("verified"),
        "created_at": u.get("created_at"),
        "profile_image_url": u.get("profile_image_url"),
        "public_metrics": {
            "followers_count": u.get("followers_count"),
            "following_count": u.get("followings_count"),
            "tweet_count": u.get("tweets_count"),
        },
    }

def v2_tweet(t):
    """Reshape a flat post into the X API v2 tweet object."""
    return {
        "id": t["id"],
        "text": t["full_text"],
        "created_at": t["created_at"],
        "lang": t.get("lang"),
        "author_id": t["user"]["id"],
        "conversation_id": t.get("conversation_id_str"),
        "public_metrics": {
            "like_count": t.get("likes_count"),
            "retweet_count": t.get("retweet_count"),
            "reply_count": t.get("reply_count"),
            "quote_count": t.get("quote_count"),
            "bookmark_count": t.get("bookmark_count"),
            "impression_count": t.get("view_count"),
        },
    }

def search_recent(query, next_token=None):
    """Drop-in for GET /2/tweets/search/recent that returns a v2-shaped page."""
    body = {"query": query, "order": "latest"}
    if next_token:
        body["next_cursor"] = next_token
    r = requests.post(f"{SORSA}/search-tweets", headers=HEADERS, json=body, timeout=30)
    r.raise_for_status()
    page = r.json()
    tweets = page.get("tweets", [])
    authors = {t["user"]["id"]: v2_user(t["user"]) for t in tweets}
    meta = {"result_count": len(tweets)}
    if page.get("next_cursor"):  # like v2, omit next_token on the last page
        meta["next_token"] = page["next_cursor"]
    return {
        "data": [v2_tweet(t) for t in tweets],
        "includes": {"users": list(authors.values())},
        "meta": meta,
    }

Existing code that reads page["data"], joins includes.users on author_id, and follows meta.next_token until it disappears keeps running unchanged. The query passed in must already use Advanced Search syntax, which is Step 5.

Step 5: Translate search operators to Advanced Search syntax

Search queries do not carry over unchanged. The X API v2 has its own operator set, while many alternative read APIs, Sorsa included, use the Advanced Search syntax that powers X's web search. The two overlap on the basics and diverge on filters, engagement thresholds, and time windows. X's own operator reference notes that the web names min_faves: and min_retweets: are rejected by the v2 API with a 400 error, so each side needs its own spelling.

X API v2Advanced Search syntaxNote
from: to: @ # $ "exact phrase" OR -term ( )SameNo change
lang:en, url:, conversation_id:, list:SameNo change
min_likes:100min_faves:100Renamed
min_reposts:50min_retweets:50Renamed
min_replies:10min_replies:10Same
is:replyfilter:replies
is:quotefilter:quote
is:retweet / -is:retweetfilter:nativeretweets / -filter:nativeretweetsReliable with from: and for recent posts
has:mediafilter:media
has:imagesfilter:images
has:video_linkfilter:native_videofilter:videos also matches external video such as YouTube
has:linksfilter:links
has:mentions, has:hashtagsfilter:mentions, filter:hashtags
is:verifiedfilter:verified or filter:blue_verifiedChoose the badge type you mean
quotes_of_tweet_id:IDquoted_tweet_id:ID
point_radius:[lon lat 10km]geocode:lat,lon,10kmCoordinate order flips
start_time / end_time parameterssince:YYYY-MM-DD / until:YYYY-MM-DD, or since_time: / until_time: with Unix secondsTime moves into the query string
since_id / until_id parameterssince_id: / max_id:max_id: includes that post, until_id excludes it
sort_order=recency or relevancy"order": "latest" or "popular" in the body
context:, entity:, retweets_of:, -is:nullcastNo direct equivalentRewrite with keywords and accounts

A small translator handles most queries automatically:

python
import re
from datetime import datetime

V2_TO_WEB = [
    (r"\bmin_likes:", "min_faves:"),
    (r"\bmin_reposts:", "min_retweets:"),
    (r"\bis:retweet\b", "filter:nativeretweets"),
    (r"\bis:reply\b", "filter:replies"),
    (r"\bis:quote\b", "filter:quote"),
    (r"\bhas:media\b", "filter:media"),
    (r"\bhas:images\b", "filter:images"),
    (r"\bhas:video_link\b", "filter:native_video"),
    (r"\bhas:links\b", "filter:links"),
    (r"\bhas:mentions\b", "filter:mentions"),
    (r"\bhas:hashtags\b", "filter:hashtags"),
    (r"\bquotes_of_tweet_id:", "quoted_tweet_id:"),
]

def to_unix(iso_ts):
    return int(datetime.fromisoformat(iso_ts.replace("Z", "+00:00")).timestamp())

def translate_query(query, start_time=None, end_time=None):
    """Convert an X API v2 query plus time params into Advanced Search syntax."""
    for pattern, replacement in V2_TO_WEB:
        query = re.sub(pattern, replacement, query)
    if start_time:
        query += f" since_time:{to_unix(start_time)}"
    if end_time:
        query += f" until_time:{to_unix(end_time)}"
    return query

print(translate_query("from:nasa has:media -is:retweet min_likes:100",
                      start_time="2026-09-01T00:00:00Z"))
# from:nasa filter:media -filter:nativeretweets min_faves:100 since_time:1788220800

Keep queries under roughly 22 operators, the practical ceiling for Advanced Search. The mentions endpoint also takes engagement and date filters as body fields (min_likes, min_replies, min_retweets, since_date, until_date), so those do not need to live in the query string. The full operator list, with examples, is in the Twitter search operators cheat sheet.

Step 6: Replace pagination tokens with next_cursor

The official API takes pagination_token (timelines, follows, lists) or next_token (search) and returns meta.next_token. Here, one field does both jobs: send next_cursor as a query parameter on GET endpoints or inside the JSON body on POST endpoints, and read next_cursor from the top level of the response. A missing or null cursor means the last page.

python
from itertools import islice

def paginate(method, path, params, key):
    """Yield items from any list endpoint until next_cursor runs out."""
    cursor = None
    while True:
        args = dict(params, next_cursor=cursor) if cursor else dict(params)
        if method == "GET":
            r = requests.get(f"{SORSA}/{path}", headers=HEADERS, params=args, timeout=30)
        else:
            r = requests.post(f"{SORSA}/{path}", headers=HEADERS, json=args, timeout=30)
        r.raise_for_status()
        page = r.json()
        yield from page.get(key, [])
        cursor = page.get("next_cursor")
        if not cursor:
            return

# Up to 200 full profiles per request: the first 10,000 followers take 50 requests
followers = list(islice(paginate("GET", "followers", {"username": "nasa"}, "users"), 10_000))

# 20 posts per request, paging past the 3,200-post timeline ceiling (a lazy generator)
posts = paginate("POST", "user-tweets", {"username": "nasa"}, "tweets")

The timeline difference matters for archives. The v2 user timeline stops at an account's most recent 3,200 posts, while POST /user-tweets keeps paging back as long as a cursor comes back. For date-bounded pulls, the historical Twitter data guide shows how to combine this with since: and until: search.

Step 7: Simplify error handling and rate limiting

The official API returns errors as an errors array or a problem object with type, title, and detail. Here, every error is one field, {"message": "..."}, with standard status codes: 400, 401, 403, 404, 429, and 500.

Rate limiting gets simpler too. Instead of per-endpoint 15-minute windows (the X API rate limits guide lists them all), each API key gets 20 requests per second across every endpoint and plan, and the counter resets every second. A 429 does not penalize the key: wait a moment and retry.

python
import time

def call(method, path, retries=4, **kwargs):
    for attempt in range(retries):
        r = requests.request(method, f"{SORSA}/{path}", headers=HEADERS, timeout=30, **kwargs)
        if r.status_code == 429:  # more than 20 requests in the current second
            time.sleep(0.25 * (2 ** attempt))
            continue
        if r.status_code >= 400:
            try:
                message = r.json().get("message")
            except ValueError:
                message = r.text
            raise RuntimeError(f"{r.status_code}: {message}")
        return r.json()
    raise RuntimeError("still rate limited after retries")

Before and after: one search call in JavaScript

The same pattern in Node.js, with the official call first and the migrated call second:

javascript
// Recent search only accepts a start_time within the last 7 days
const since = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000);

// Before: X API v2 recent search, authors joined from includes
const params = new URLSearchParams({
  query: "from:nasa has:media -is:retweet",
  start_time: since.toISOString().replace(/\.\d{3}Z$/, "Z"),
  max_results: "100",
  "tweet.fields": "created_at,public_metrics",
  expansions: "author_id",
  "user.fields": "username",
});
const before = await fetch(`https://api.x.com/2/tweets/search/recent?${params}`, {
  headers: { Authorization: `Bearer ${process.env.X_BEARER_TOKEN}` },
});
const v2 = await before.json();
const users = new Map((v2.includes?.users ?? []).map((u) => [u.id, u]));
for (const t of v2.data ?? []) {
  console.log(users.get(t.author_id)?.username, t.public_metrics.like_count, t.text);
}

// After: POST with a JSON body, author embedded in every post
const after = await fetch("https://api.sorsa.io/v3/search-tweets", {
  method: "POST",
  headers: { ApiKey: process.env.SORSA_API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({
    query: `from:nasa filter:media -filter:nativeretweets since_time:${Math.floor(since / 1000)}`,
    order: "latest",
  }),
});
const flat = await after.json();
for (const t of flat.tweets) {
  console.log(t.user.username, t.likes_count, t.full_text);
}

The join table disappears because the author ships inside every post. Full walkthroughs for each language are in the Twitter API in Python and Twitter API in Node.js guides.

How do you cut over without breaking production?

Migrate one endpoint at a time, prove each one against the official API, and keep a rollback path until the numbers match.

  1. Put one client module in front of every data call. Application code should call get_followers() or search(), never a URL. The shim from Step 4 is a good start.
  2. Shadow-read for three to seven days. Send the same inputs to both APIs and log differences without serving the new data. The official side still bills per resource, so sample a few queries instead of mirroring all traffic.
  3. Diff post IDs, not rankings. Compare results ordered by latest inside a fixed time window.
  4. Flip one endpoint behind a flag. Keep the official path callable so any endpoint can roll back on its own.
  5. Watch quota after each flip. GET /key-usage-info returns the requests remaining on your key.
  6. Delete the OAuth read code last, once every read endpoint has run clean for a couple of weeks.

A minimal diff for step 3, reusing translate_query() and paginate() from above:

python
def shadow_diff(v2_query, start_time, pages=5):
    """Run one query on both APIs and compare post IDs before cutting over.

    start_time must fall inside recent search's 7-day window.
    """
    r = requests.get(
        "https://api.x.com/2/tweets/search/recent",
        headers={"Authorization": f"Bearer {os.environ['X_BEARER_TOKEN']}"},
        params={"query": v2_query, "start_time": start_time, "max_results": 100},
        timeout=30,
    )
    r.raise_for_status()  # a failed official call must not look like "0 results"
    official_ids = {t["id"] for t in r.json().get("data", [])}

    body = {"query": translate_query(v2_query, start_time), "order": "latest"}
    new_ids = set()
    for tweet in paginate("POST", "search-tweets", body, "tweets"):
        new_ids.add(tweet["id"])
        if len(new_ids) >= pages * 20:
            break

    shared = official_ids & new_ids
    print(f"official={len(official_ids)} new={len(new_ids)} shared={len(shared)}")
    print("only on official:", sorted(official_ids - new_ids)[:10])
    return shared

Each official run reads up to 100 posts, about $0.50 at pay-per-use rates. Expect small, explainable gaps: engagement counts move between calls, the two indexes rank "popular" results differently, and X's own index stopped returning retweets in keyword search in May 2026.

Run the diff on your own queries. The 100 free requests on a new Sorsa account cover a full parallel test of search, timelines, and follower pulls. To see real responses before writing any code, send the same calls from the API playground in your browser.

What stays on the official X API?

Everything a typical data pipeline reads can move: search, timelines, profiles, followers, lists, replies, quotes, reposters, trends, and Spaces. What stays on X is the write side and data only the account owner can see, which is usually a small bill:

CapabilityWhy it staysCost on pay-per-use
Posting and repliesRead-only APIs do not write$0.015 per post, $0.20 with a URL, $0.010 per summoned reply
Direct messagesPrivate data requires the account owner's authorization$0.015 per DM sent, $0.010 per DM event read
Follows, likes, quote-postsRemoved from self-serve plans on April 20, 2026Enterprise contract only
Your account's private data (bookmarks, home timeline)Only the account owner can authorize it$0.001 per resource
Filtered stream and push webhooksReal-time push deliveryMetered under pay-per-use, see the rate card
Ads API and compliance programsOfficial-only programsSeparate agreements

Keep one small credit balance on X for these, with a spending limit so a busy week cannot surprise you. For alerting and dashboards, polling search on a flat plan replaces the stream for most monitoring jobs, as the real-time Twitter monitoring guide shows.

X API migration checklist

  1. Record your cutover date, spending limit, and auto-recharge rule in the X Developer Console.
  2. Export a month of usage by resource type: posts, users, followers.
  3. Mark every write call (posting, replies, DMs) to stay on X.
  4. Replace Authorization: Bearer ... with the ApiKey header on read calls.
  5. Remove OAuth 1.0a signing and token refresh from the read path.
  6. Change the base URL to https://api.sorsa.io/v3.
  7. Remap every endpoint with the Step 2 table.
  8. Switch post and search calls from GET to POST with JSON bodies.
  9. Delete tweet.fields, user.fields, and expansions parameters.
  10. Flatten parsers, or route calls through the v2 shim.
  11. Translate search operators and move start_time and end_time into the query.
  12. Replace pagination_token and next_token with next_cursor.
  13. Update error handling for the single message field, and throttle to 20 requests per second.
  14. Shadow-read and diff for three to seven days, then flip endpoints one at a time.
  15. Track quota with GET /key-usage-info after every flip.

FAQ

What changed in the Twitter/X API in 2026?

X launched pay-per-use billing on February 6, 2026, then moved legacy Basic plans onto it after June 1 and legacy Pro plans after September 1. On April 20 it removed follow, like, and quote-post writes from self-serve plans and repriced posting. On May 4 search moved to a new index with new engagement operators, and Communities shut down on May 30.

What happens when a legacy Basic or Pro plan moves to pay-per-use?

The endpoints stay the same, but billing switches from a flat monthly fee to per-resource charges: $0.005 per post read and $0.010 per user or follower read. Annual subscribers receive the prorated unused balance as credits that expire after the matching number of months. Post reads are capped at 3 million per billing cycle, so review the spending limit and auto-recharge settings right after the cutover.

Is there an alternative to X API pay-per-use for read-heavy apps?

Yes. Flat per-request APIs bill one request per call instead of one charge per post or profile. Sorsa API, for example, costs from $0.02 per 1,000 tweets and from $0.01 per 1,000 profiles, uses a single API key, allows 20 requests per second on every plan, and includes 100 free requests to test. Posting and DMs still go through the official X API.

Why did X API search stop returning retweets?

On May 4, 2026, X moved its recent and full-archive search endpoints to a new index, and keyword searches stopped returning retweets. Filtered stream was not affected. To count reposts of a specific post, use the reposted-by endpoint. To find recent retweets by a known account, query from:username filter:nativeretweets on an API that supports Advanced Search syntax.

Do X API v2 search queries work on other APIs without changes?

Keywords, from:, to:, hashtags, quoted phrases, OR, and exclusions carry over, but several operators need renaming. APIs built on X's Advanced Search syntax use min_faves: instead of min_likes:, min_retweets: instead of min_reposts:, and filter:media or filter:replies instead of has:media or is:reply. Time filters move from start_time and end_time parameters into since: and until: operators.

Can you migrate off the X API one endpoint at a time?

Yes, and it is the safer route. Put a thin client in front of your data calls, switch one endpoint, run it in parallel with the official API for a few days, compare the output, then move the next one. Each endpoint can roll back on its own, and write calls never have to move.

How do you keep posting to X if the new API is read-only?

Keep the official X API for writes and move only the read path. Posting, replies, DMs, and your own account's data stay on X under pay-per-use, while search, timelines, profiles, and follower lists run through a flat per-request read API. Most teams keep one small X credit balance, with a spending limit, for the write side.

Does moving off the official API remove the 3,200-post timeline limit?

It can. The X API v2 user timeline endpoint returns only an account's most recent 3,200 posts. Sorsa's user-tweets endpoint has no such ceiling: keep passing next_cursor until the response stops returning one to page back through older posts. For a specific date range, historical search with since: and until: operators is faster.

How long does an X API migration take?

A read-only pipeline usually takes one to three engineering days: a few hours for auth, the base URL, and endpoint mapping, about a day for response parsing and search operators, and a day of parallel testing before cutover. Apps that also write keep those calls on X, so the work stays limited to the read path.

Getting started

The fastest way to scope a migration is to run your three most expensive calls through both APIs and compare the output. Sorsa starts with 100 free requests: one-time, no card, no expiry, enough to pull up to 10,000 tweets or 20,000 profiles while you run a parallel test of search, timelines, and follower pulls. Get an API key and make a first request in minutes.

When you are ready to scale, the flat per-request pricing plans run from about $0.02 per 1,000 tweets and $0.01 per 1,000 profiles on batch endpoints, with the same 20 requests per second on every plan, and you don't need to apply for an X developer account. If the data feeds an AI agent, connect the same endpoints through the Sorsa MCP server.

Reviewed by Keksich, founder of Sorsa, marketer and X API researcher.

This guide draws on our team's hands-on work running the live Sorsa API since 2022 and moving read pipelines off the official X API. Every X API fact was checked on September 23, 2026 against X's changelog, pay-per-use pricing documentation, search operator reference, and data dictionary, and the legacy Basic and Pro cutover dates come from X's developer-forum announcements. Sorsa endpoint paths, parameters, and response fields were checked against the v3 API documentation, the Python samples were unit-tested against mocked API responses, and the JavaScript sample was syntax-checked and mock-run in Node.js 22. Sorsa is an independent service and is not affiliated with or endorsed by X Corp. Verified September 2026.