Skip to main content

Endpoint, authentication, response format, and pagination mapping for moving from the official Twitter/X API v2 to Sorsa API v3

This page is a reference for moving an existing integration from the official Twitter/X API v2 to Sorsa API v3. It covers authentication, endpoint mapping, response format changes, pagination, HTTP method differences, search query syntax, error handling, and code examples in curl, Python, and JavaScript. Sorsa API is read-only. If your integration also writes to X (posts tweets, sends DMs, likes, follows), keep the official API key for the write path and migrate only the read path.
Note: For a narrative migration walkthrough with cost comparisons and step-by-step examples, see Migrating from the Twitter/X API: Complete Developer Guide on the blog.

Overview of changes

ConcernOfficial X API v2Sorsa API v3
Base URLhttps://api.x.com/2https://api.sorsa.io/v3
AuthenticationOAuth 2.0 Bearer / OAuth 1.0aAPI key in ApiKey header
Field selectiontweet.fields, user.fields, expansionsAll fields returned by default
Response envelopedata + includes + metaFlat object, embedded relations
Paginationpagination_token / meta.next_tokennext_cursor (top-level)
Rate limitsPer-endpoint, per-15-min windowsUniversal 20 req/s
Error shapeerrors[] with type, title, detail{ "message": "..." }

Authentication

The official API uses OAuth 2.0 Bearer tokens for app-only requests, and OAuth 1.0a User Context for user-scoped requests.
# Official API (OAuth 2.0 App-Only)
curl "https://api.x.com/2/users/by/username/elonmusk" \
  -H "Authorization: Bearer $BEARER_TOKEN"
Sorsa API uses a single API key passed in the ApiKey header. Generate keys in the dashboard.
curl "https://api.sorsa.io/v3/info?username=elonmusk" \
  -H "ApiKey: $API_KEY"
import requests

response = requests.get(
    "https://api.sorsa.io/v3/info",
    params={"username": "elonmusk"},
    headers={"ApiKey": API_KEY},
)
user = response.json()
const response = await fetch("https://api.sorsa.io/v3/info?username=elonmusk", {
  headers: { ApiKey: API_KEY },
});
const user = await response.json();
See Authentication for full details.

Endpoint mapping

Users

ActionOfficial X API v2Sorsa API v3
Get user by usernameGET /2/users/by/username/:usernameGET /info?username=:username
Get user by IDGET /2/users/:idGET /info?user_id=:id
Get multiple usersGET /2/users?ids=...GET /info-batch?user_ids=...&user_ids=...
Get followersGET /2/users/:id/followersGET /followers?user_id=:id
Get 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 accepts up to 100 usernames or IDs per request. Repeat the query parameter: ?usernames=a&usernames=b.
  • GET /followers and GET /follows return up to 200 fully-hydrated profiles per page, including bio, follower counts, and verification status.

Tweets

ActionOfficial X API v2Sorsa API v3
Get a single tweetGET /2/tweets/:idPOST /tweet-info body: { "tweet_link": ":id" }
Get multiple tweetsGET /2/tweets?ids=...POST /tweet-info-bulk body: { "tweet_links": [...] }
User timelineGET /2/users/:id/tweetsPOST /user-tweets body: { "user_id": ":id" }
Quote tweetsGET /2/tweets/:id/quote_tweetsPOST /quotes body: { "tweet_link": ":id" }
RetweetersGET /2/tweets/:id/retweeted_byPOST /retweeters body: { "tweet_link": ":id" }
Replies (comments)Not a dedicated endpointPOST /comments body: { "tweet_link": ":id" }
Long-form X ArticleNot availablePOST /article body: { "tweet_link": ":id" }
  • tweet_link accepts either a full tweet URL (https://x.com/user/status/123) or just the numeric ID ("123").
  • POST /tweet-info-bulk returns up to 100 tweets per request. Use it instead of looping POST /tweet-info to reduce request count by up to 100x.
  • POST /user-tweets has no 3,200-tweet ceiling. Paginate until next_cursor is absent to retrieve the full timeline back to the account’s first tweet. See Historical data.
ActionOfficial X API v2Sorsa API v3
Search recent tweetsGET /2/tweets/search/recent?query=...POST /search-tweets body: { "query": "..." }
Search full archiveGET /2/tweets/search/all?query=...POST /search-tweets (historical included)
Search mentionsGET .../search/recent?query=@userPOST /mentions body: { "query": "user" }
Search usersNot available in v2POST /search-users body: { "query": "..." }
  • Sorsa supports the same Twitter Advanced Search operators on /search-tweets. Query strings transfer unchanged. See Search operators.
  • POST /mentions adds filters not available on the official API: min_likes, min_replies, min_retweets, since_date, until_date.

Lists

ActionOfficial X API v2Sorsa API v3
List membersGET /2/lists/:id/membersGET /list-members?list_id=:id
List followersGET /2/lists/:id/followersGET /list-followers?list_link=:id
List tweetsGET /2/lists/:id/tweetsGET /list-tweets?list_id=:id

Communities

The official X API does not expose Community endpoints. Sorsa-only.
ActionSorsa API v3
Community membersPOST /community-members body: { "community_link": ":id" }
Community feedPOST /community-tweets body: { "community_id": ":id", "order": "popular" }
Search within a communityPOST /community-search-tweets body: { "community_link": ":id", "query": "..." }
See Lists and Communities.

Verification

These check membership-style questions in a single call. The official API has no equivalent; replicating them requires fetching full lists and scanning client-side.
QuestionSorsa API v3
Does user A follow user B?POST /check-follow
Did user X comment on tweet Y?GET /check-comment?tweet_link=...&username=...
Did user X quote or retweet tweet Y?POST /check-quoted
Did user X retweet tweet Y?POST /check-retweet
Is user X in community Y?POST /check-community-member
See Marketing campaign verification.

Analytics (Sorsa-only)

ActionSorsa API v3
Influence scoreGET /score?username=...
Score deltas (7d, 30d)GET /score-changes?username=...
Follower breakdown by categoryGET /followers-stats?username=...
Top 20 followers by scoreGET /top-followers?username=...
Top 20 following by scoreGET /top-following?username=...
New followers (7 days)GET /new-followers-7d?username=...
New following (7 days)GET /new-following-7d?username=...
These endpoints index a crypto-focused subset of accounts (influencers, projects, VCs). See Sorsa Score and crypto analytics.

Utility

ActionSorsa API v3
Username to numeric IDGET /username-to-id/:handle
Numeric ID to usernameGET /id-to-username/:id
Profile URL to numeric IDGET /link-to-id?link=...
API key usage statsGET /key-usage-info
See ID conversion.

Response format changes

The largest single change in the migration. Official v2 responses are wrapped in data, includes, and meta. Sorsa returns flat objects with author data embedded directly in each tweet.

User profile

Official X API v2 (with field selection):
{
  "data": {
    "id": "44196397",
    "name": "Elon Musk",
    "username": "elonmusk",
    "verified": false,
    "profile_image_url": "https://pbs.twimg.com/...",
    "public_metrics": {
      "followers_count": 100000000,
      "following_count": 500,
      "tweet_count": 30000,
      "listed_count": 12000
    }
  }
}
Sorsa API v3:
{
  "id": "44196397",
  "username": "elonmusk",
  "display_name": "Elon Musk",
  "description": "...",
  "location": "Austin, TX",
  "profile_image_url": "https://pbs.twimg.com/...",
  "profile_background_image_url": "...",
  "followers_count": 100000000,
  "followings_count": 500,
  "tweets_count": 30000,
  "favourites_count": 50000,
  "media_count": 1200,
  "verified": false,
  "protected": false,
  "can_dm": true,
  "possibly_sensitive": false,
  "created_at": "2009-06-02T20:12:29Z",
  "bio_urls": ["https://example.com"],
  "pinned_tweet_ids": ["17823..."]
}

Tweet

Official X API v2 (with expansions=author_id):
{
  "data": {
    "id": "1234567890",
    "text": "Hello world",
    "created_at": "2024-01-15T12:00:00.000Z",
    "author_id": "44196397",
    "conversation_id": "1234567890",
    "lang": "en",
    "public_metrics": {
      "retweet_count": 100,
      "reply_count": 50,
      "like_count": 500,
      "quote_count": 25,
      "bookmark_count": 10,
      "impression_count": 50000
    }
  },
  "includes": {
    "users": [
      { "id": "44196397", "name": "Elon Musk", "username": "elonmusk" }
    ]
  }
}
Sorsa API v3:
{
  "id": "1234567890",
  "full_text": "Hello world",
  "created_at": "2024-01-15T12:00:00Z",
  "lang": "en",
  "conversation_id_str": "1234567890",
  "likes_count": 500,
  "retweet_count": 100,
  "reply_count": 50,
  "quote_count": 25,
  "view_count": 50000,
  "bookmark_count": 10,
  "is_reply": false,
  "is_quote_status": false,
  "is_replies_limited": false,
  "in_reply_to_tweet_id": null,
  "in_reply_to_username": null,
  "user": {
    "id": "44196397",
    "username": "elonmusk",
    "display_name": "Elon Musk",
    "followers_count": 100000000
  },
  "entities": [],
  "quoted_status": null,
  "retweeted_status": null
}

Field mapping

User fields

Official X API v2Sorsa API v3Notes
ididSame
usernameusernameSame
namedisplay_nameRenamed
descriptiondescriptionSame
locationlocationSame
verifiedverifiedSame
protectedprotectedSame
profile_image_urlprofile_image_urlSame
created_atcreated_atSame
public_metrics.followers_countfollowers_countFlattened
public_metrics.following_countfollowings_countFlattened, renamed
public_metrics.tweet_counttweets_countFlattened, renamed
public_metrics.listed_countNot available
Not availablefavourites_countSorsa-only
Not availablemedia_countSorsa-only
Not availablecan_dmSorsa-only
Not availablebio_urlsSorsa-only
Not availablepinned_tweet_idsSorsa-only
Not availableprofile_background_image_urlSorsa-only
Not availablepossibly_sensitiveSorsa-only

Tweet fields

Official X API v2Sorsa API v3Notes
ididSame
textfull_textRenamed
created_atcreated_atSame
langlangSame
conversation_idconversation_id_strRenamed
in_reply_to_user_idin_reply_to_usernameReturns handle instead of numeric ID
public_metrics.like_countlikes_countFlattened, renamed (note plural)
public_metrics.retweet_countretweet_countFlattened
public_metrics.reply_countreply_countFlattened
public_metrics.quote_countquote_countFlattened
public_metrics.bookmark_countbookmark_countFlattened
public_metrics.impression_countview_countFlattened, renamed
author_id + includes.users[]user (full object inline)Embedded
Referenced tweets via includesquoted_status, retweeted_statusInline objects
Not availableis_reply, is_quote_status, is_replies_limitedSorsa-only booleans
Not availablein_reply_to_tweet_idSorsa-only
entities (urls, mentions, hashtags, media)entities array of { type, link, preview }Different shape

Pagination

The official API uses pagination_token in query parameters and returns meta.next_token. Sorsa uses a single field, next_cursor, in both directions. For GET endpoints, pass next_cursor as a query parameter:
curl "https://api.sorsa.io/v3/followers?username=elonmusk&next_cursor=ABC123" \
  -H "ApiKey: $API_KEY"
For POST endpoints, include next_cursor 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 always returns next_cursor at the top level:
{
  "tweets": [...],
  "next_cursor": "XYZ789"
}
When next_cursor is missing or null, there are no more pages. See Pagination for details.

HTTP method differences

Some endpoints that are GET on the official API are POST on Sorsa.
ActionOfficial APISorsa API
Get a tweetGETPOST
Search tweetsGETPOST
User timelineGETPOST
Quote tweetsGETPOST
RetweetersGETPOST
Replies (comments)(no direct equivalent)POST
User profileGETGET
Followers / followingGETGET
ListsGETGET
Rule of thumb: endpoints that take a tweet identifier or a search query are POST. Endpoints that take a user identifier or a list ID are GET.

Code migration examples

Get a user profile

Before (Official API):
curl "https://api.x.com/2/users/by/username/elonmusk?user.fields=description,public_metrics,profile_image_url,verified,created_at" \
  -H "Authorization: Bearer $BEARER_TOKEN"
import requests

response = requests.get(
    "https://api.x.com/2/users/by/username/elonmusk",
    params={"user.fields": "description,public_metrics,profile_image_url,verified,created_at"},
    headers={"Authorization": f"Bearer {BEARER_TOKEN}"},
)
user = response.json()["data"]
followers = user["public_metrics"]["followers_count"]
name = user["name"]
const url = "https://api.x.com/2/users/by/username/elonmusk" +
  "?user.fields=description,public_metrics,profile_image_url,verified,created_at";
const res = await fetch(url, {
  headers: { Authorization: `Bearer ${BEARER_TOKEN}` },
});
const { data: user } = await res.json();
const followers = user.public_metrics.followers_count;
const name = user.name;
After (Sorsa API):
curl "https://api.sorsa.io/v3/info?username=elonmusk" \
  -H "ApiKey: $API_KEY"
import requests

response = requests.get(
    "https://api.sorsa.io/v3/info",
    params={"username": "elonmusk"},
    headers={"ApiKey": API_KEY},
)
user = response.json()
followers = user["followers_count"]
name = user["display_name"]
const res = await fetch("https://api.sorsa.io/v3/info?username=elonmusk", {
  headers: { ApiKey: API_KEY },
});
const user = await res.json();
const followers = user.followers_count;
const name = user.display_name;

Search tweets

Before:
curl "https://api.x.com/2/tweets/search/recent?query=from%3Aelonmusk%20since%3A2024-01-01&tweet.fields=created_at,public_metrics,lang&expansions=author_id&user.fields=username,name&max_results=10" \
  -H "Authorization: Bearer $BEARER_TOKEN"
params = {
    "query": "from:elonmusk since:2024-01-01",
    "tweet.fields": "created_at,public_metrics,lang",
    "expansions": "author_id",
    "user.fields": "username,name",
    "max_results": 10,
}
response = requests.get(
    "https://api.x.com/2/tweets/search/recent",
    headers={"Authorization": f"Bearer {BEARER_TOKEN}"},
    params=params,
)
data = response.json()

tweets = data["data"]
users = {u["id"]: u for u in data.get("includes", {}).get("users", [])}
next_token = data.get("meta", {}).get("next_token")

for tweet in tweets:
    author = users.get(tweet["author_id"])
    print(tweet["text"], "by", author["username"])
const params = new URLSearchParams({
  query: "from:elonmusk since:2024-01-01",
  "tweet.fields": "created_at,public_metrics,lang",
  expansions: "author_id",
  "user.fields": "username,name",
  max_results: "10",
});
const res = await fetch(`https://api.x.com/2/tweets/search/recent?${params}`, {
  headers: { Authorization: `Bearer ${BEARER_TOKEN}` },
});
const data = await res.json();

const tweets = data.data || [];
const users = Object.fromEntries((data.includes?.users || []).map(u => [u.id, u]));

for (const t of tweets) {
  const author = users[t.author_id];
  console.log(t.text, "by", author.username);
}
After:
curl -X POST "https://api.sorsa.io/v3/search-tweets" \
  -H "ApiKey: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "from:elonmusk since:2024-01-01"}'
response = requests.post(
    "https://api.sorsa.io/v3/search-tweets",
    headers={"ApiKey": API_KEY},
    json={"query": "from:elonmusk since:2024-01-01"},
)
data = response.json()

for tweet in data["tweets"]:
    print(tweet["full_text"], "by", tweet["user"]["username"])
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);
}

Paginate through all followers

Before:
def fetch_all_followers_official(user_id, token):
    url = f"https://api.x.com/2/users/{user_id}/followers"
    headers = {"Authorization": f"Bearer {token}"}
    followers = []
    pagination_token = None

    while True:
        params = {"max_results": 1000}
        if pagination_token:
            params["pagination_token"] = pagination_token

        r = requests.get(url, headers=headers, params=params)
        r.raise_for_status()
        data = r.json()

        followers.extend(data.get("data", []))
        pagination_token = data.get("meta", {}).get("next_token")
        if not pagination_token:
            break

    return followers
After:
CURSOR=""
while :; do
  RES=$(curl -s "https://api.sorsa.io/v3/followers?username=elonmusk${CURSOR:+&next_cursor=$CURSOR}" \
    -H "ApiKey: $API_KEY")
  echo "$RES" | jq '.users'
  CURSOR=$(echo "$RES" | jq -r '.next_cursor // empty')
  [ -z "$CURSOR" ] && break
done
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

        r = requests.get(url, headers=headers, params=params)
        r.raise_for_status()
        data = r.json()

        followers.extend(data.get("users", []))
        next_cursor = data.get("next_cursor")
        if not next_cursor:
            break

    return followers
async function fetchAllFollowers(userId, apiKey) {
  const followers = [];
  let nextCursor = null;

  do {
    const params = new URLSearchParams({ user_id: userId });
    if (nextCursor) params.set("next_cursor", nextCursor);

    const res = await fetch(
      `https://api.sorsa.io/v3/followers?${params}`,
      { headers: { ApiKey: apiKey } }
    );
    const json = await res.json();

    followers.push(...(json.users || []));
    nextCursor = json.next_cursor || null;
  } while (nextCursor);

  return followers;
}
Each Sorsa page returns up to 200 fully-hydrated profiles. The official API typically returns IDs and minimal user data, requiring a separate lookup to hydrate profiles.

Search query syntax

Sorsa supports the same Advanced Search operators as the official API. Existing query strings transfer unchanged.
OperatorExample
from:from:elonmusk
to:to:elonmusk
since: / until:since:2024-01-01 until:2024-02-01
Hashtag#bitcoin
Exact phrase"hello world"
ORbitcoin OR ethereum
Exclusion-is:retweet
Combinedfrom:elonmusk #bitcoin -is:retweet
Full reference: Search operators. The /mentions endpoint additionally supports server-side filters: min_likes, min_replies, min_retweets, since_date, until_date. See Track mentions.

Error handling

The official API returns errors as a structured errors array:
{
  "errors": [
    {
      "message": "Not Found",
      "type": "https://api.x.com/2/problems/resource-not-found",
      "title": "Not Found Error",
      "detail": "Could not find tweet with id: [123].",
      "status": 404
    }
  ]
}
Sorsa returns a simple shape:
{ "message": "Tweet not found" }
Status codes are consistent across all endpoints: 400, 401, 403, 404, 429, 500. See Error codes. Rate limit handling: on 429, wait one second and retry. The universal limit is 20 req/s across all endpoints. There are no per-endpoint windows to track. See Rate limits. A retry wrapper that handles both APIs:
import time
import 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")

Migration checklist

  • Replace Authorization: Bearer ... with ApiKey: ....
  • Remove OAuth 1.0a signature logic (consumer keys, access tokens, signature generation).
  • Update base URL from https://api.x.com/2 to https://api.sorsa.io/v3.
  • Map every endpoint path using the tables above.
  • Switch GET to POST for tweet, search, comment, quote, retweeter endpoints.
  • Remove tweet.fields, user.fields, media.fields, and expansions parameters.
  • Update response parsers: remove data / includes / meta unwrapping.
  • Rename fields (namedisplay_name, textfull_text, etc.).
  • Flatten metric access (drop the public_metrics wrapper).
  • Replace pagination_token / next_token with next_cursor.
  • Update error handling for the simplified { "message": "..." } format.
  • Adjust rate-limit logic: 20 req/s universal, no per-endpoint windows.
  • Test critical endpoints in the API Playground.
  • Monitor quota via GET /key-usage-info.
  • Retain the official API key for write operations (posting, DMs) if needed.

Features without an official API equivalent

FeatureEndpoint
Tweet timeline without 3,200-tweet capPOST /user-tweets
Single-call follow checkPOST /check-follow
Single-call retweet checkPOST /check-retweet
Single-call comment checkGET /check-comment
Single-call quote/retweet checkPOST /check-quoted
Community membership checkPOST /check-community-member
Community members and feedPOST /community-members, POST /community-tweets
In-community searchPOST /community-search-tweets
Long-form X Article contentPOST /article
Verified-only follower filterGET /verified-followers
Account country and username change historyGET /about
Influence scoreGET /score, GET /score-changes
Top followers and following by influenceGET /top-followers, GET /top-following
Categorized follower breakdownGET /followers-stats