> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sorsa.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Migration from Official X API

# Migrating from the Official 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. Every new account includes 100 free requests (no card required), so you can validate the mapped endpoints before switching production traffic over.

> **Note:** For a narrative migration walkthrough with cost comparisons and step-by-step examples, see [Migrating from the Twitter/X API: Complete Developer Guide](https://api.sorsa.io/blog/migrate-from-twitter-api) on the blog.

## Overview of changes

| Concern           | Official X API v2                           | Sorsa API v3                    |
| ----------------- | ------------------------------------------- | ------------------------------- |
| Base URL          | `https://api.x.com/2`                       | `https://api.sorsa.io/v3`       |
| Authentication    | OAuth 2.0 Bearer / OAuth 1.0a               | API key in `ApiKey` header      |
| Field selection   | `tweet.fields`, `user.fields`, `expansions` | All fields returned by default  |
| Response envelope | `data` + `includes` + `meta`                | Flat object, embedded relations |
| Pagination        | `pagination_token` / `meta.next_token`      | `next_cursor` (top-level)       |
| Rate limits       | Per-endpoint, per-15-min windows            | Universal 20 req/s              |
| Error shape       | `errors[]` 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.

```bash theme={null}
# 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](https://api.sorsa.io/overview/keys).

```bash theme={null}
curl "https://api.sorsa.io/v3/info?username=elonmusk" \
  -H "ApiKey: $API_KEY"
```

```python theme={null}
import requests

response = requests.get(
    "https://api.sorsa.io/v3/info",
    params={"username": "elonmusk"},
    headers={"ApiKey": API_KEY},
)
user = response.json()
```

```javascript theme={null}
const response = await fetch("https://api.sorsa.io/v3/info?username=elonmusk", {
  headers: { ApiKey: API_KEY },
});
const user = await response.json();
```

See [Authentication](https://docs.sorsa.io/authentication) for full details.

## Endpoint mapping

### Users

| Action                   | Official X API v2                    | Sorsa API v3                                |
| ------------------------ | ------------------------------------ | ------------------------------------------- |
| Get user by username     | `GET /2/users/by/username/:username` | `GET /info?username=:username`              |
| Get user by ID           | `GET /2/users/:id`                   | `GET /info?user_id=:id`                     |
| Get multiple users       | `GET /2/users?ids=...`               | `GET /info-batch?user_ids=...&user_ids=...` |
| Get followers            | `GET /2/users/:id/followers`         | `GET /followers?user_id=:id`                |
| Get 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` 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

| Action              | Official X API v2                | Sorsa API v3                                             |
| ------------------- | -------------------------------- | -------------------------------------------------------- |
| Get a single tweet  | `GET /2/tweets/:id`              | `POST /tweet-info` body: `{ "tweet_link": ":id" }`       |
| Get multiple tweets | `GET /2/tweets?ids=...`          | `POST /tweet-info-bulk` body: `{ "tweet_links": [...] }` |
| User timeline       | `GET /2/users/:id/tweets`        | `POST /user-tweets` body: `{ "user_id": ":id" }`         |
| Quote tweets        | `GET /2/tweets/:id/quote_tweets` | `POST /quotes` body: `{ "tweet_link": ":id" }`           |
| Retweeters          | `GET /2/tweets/:id/retweeted_by` | `POST /retweeters` body: `{ "tweet_link": ":id" }`       |
| Replies (comments)  | Not a dedicated endpoint         | `POST /comments` body: `{ "tweet_link": ":id" }`         |
| Long-form X Article | Not available                    | `POST /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](https://docs.sorsa.io/historical-data).

### Search

| Action               | Official X API v2                       | Sorsa API v3                                     |
| -------------------- | --------------------------------------- | ------------------------------------------------ |
| Search recent tweets | `GET /2/tweets/search/recent?query=...` | `POST /search-tweets` body: `{ "query": "..." }` |
| Search full archive  | `GET /2/tweets/search/all?query=...`    | `POST /search-tweets` (historical included)      |
| Search mentions      | `GET .../search/recent?query=@user`     | `POST /mentions` body: `{ "query": "user" }`     |
| Search users         | Not available in v2                     | `POST /search-users` body: `{ "query": "..." }`  |

* Sorsa supports the same Twitter Advanced Search operators on `/search-tweets`. Query strings transfer unchanged. See [Search operators](https://docs.sorsa.io/search-operators).
* `POST /mentions` adds filters not available on the official API: `min_likes`, `min_replies`, `min_retweets`, `since_date`, `until_date`.

### Lists

| Action         | Official X API v2            | Sorsa API v3                        |
| -------------- | ---------------------------- | ----------------------------------- |
| List members   | `GET /2/lists/:id/members`   | `GET /list-members?list_id=:id`     |
| List followers | `GET /2/lists/:id/followers` | `GET /list-followers?list_link=:id` |
| List tweets    | `GET /2/lists/:id/tweets`    | `GET /list-tweets?list_id=:id`      |

### Communities

The official X API does not expose Community endpoints. Sorsa-only.

| Action                    | Sorsa API v3                                                                        |
| ------------------------- | ----------------------------------------------------------------------------------- |
| Community members         | `POST /community-members` body: `{ "community_link": ":id" }`                       |
| Community feed            | `POST /community-tweets` body: `{ "community_id": ":id", "order": "popular" }`      |
| Search within a community | `POST /community-search-tweets` body: `{ "community_link": ":id", "query": "..." }` |

See [Lists and Communities](https://docs.sorsa.io/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.

| Question                             | Sorsa 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](https://docs.sorsa.io/Marketing-Campaign-Verification).

### Analytics (Sorsa-only)

| Action                         | Sorsa API v3                         |
| ------------------------------ | ------------------------------------ |
| Influence score                | `GET /score?username=...`            |
| Score deltas (7d, 30d)         | `GET /score-changes?username=...`    |
| Follower breakdown by category | `GET /followers-stats?username=...`  |
| Top 20 followers by score      | `GET /top-followers?username=...`    |
| Top 20 following by score      | `GET /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](https://docs.sorsa.io/sorsa-score-and-crypto-analytics).

### Utility

| Action                    | Sorsa API v3                  |
| ------------------------- | ----------------------------- |
| Username to numeric ID    | `GET /username-to-id/:handle` |
| Numeric ID to username    | `GET /id-to-username/:id`     |
| Profile URL to numeric ID | `GET /link-to-id?link=...`    |
| API key usage stats       | `GET /key-usage-info`         |

See [ID conversion](https://docs.sorsa.io/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):

```json theme={null}
{
  "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:**

```json theme={null}
{
  "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`):

```json theme={null}
{
  "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:**

```json theme={null}
{
  "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 v2                | Sorsa API v3                   | Notes              |
| -------------------------------- | ------------------------------ | ------------------ |
| `id`                             | `id`                           | Same               |
| `username`                       | `username`                     | Same               |
| `name`                           | `display_name`                 | Renamed            |
| `description`                    | `description`                  | Same               |
| `location`                       | `location`                     | Same               |
| `verified`                       | `verified`                     | Same               |
| `protected`                      | `protected`                    | Same               |
| `profile_image_url`              | `profile_image_url`            | Same               |
| `created_at`                     | `created_at`                   | Same               |
| `public_metrics.followers_count` | `followers_count`              | Flattened          |
| `public_metrics.following_count` | `followings_count`             | Flattened, renamed |
| `public_metrics.tweet_count`     | `tweets_count`                 | Flattened, renamed |
| `public_metrics.listed_count`    | Not available                  |                    |
| Not available                    | `favourites_count`             | Sorsa-only         |
| Not available                    | `media_count`                  | Sorsa-only         |
| Not available                    | `can_dm`                       | Sorsa-only         |
| Not available                    | `bio_urls`                     | Sorsa-only         |
| Not available                    | `pinned_tweet_ids`             | Sorsa-only         |
| Not available                    | `profile_background_image_url` | Sorsa-only         |
| Not available                    | `possibly_sensitive`           | Sorsa-only         |

#### Tweet fields

| Official X API v2                            | Sorsa API v3                                        | Notes                                |
| -------------------------------------------- | --------------------------------------------------- | ------------------------------------ |
| `id`                                         | `id`                                                | Same                                 |
| `text`                                       | `full_text`                                         | Renamed                              |
| `created_at`                                 | `created_at`                                        | Same                                 |
| `lang`                                       | `lang`                                              | Same                                 |
| `conversation_id`                            | `conversation_id_str`                               | Renamed                              |
| `in_reply_to_user_id`                        | `in_reply_to_username`                              | Returns handle instead of numeric ID |
| `public_metrics.like_count`                  | `likes_count`                                       | Flattened, renamed (note plural)     |
| `public_metrics.retweet_count`               | `retweet_count`                                     | Flattened                            |
| `public_metrics.reply_count`                 | `reply_count`                                       | Flattened                            |
| `public_metrics.quote_count`                 | `quote_count`                                       | Flattened                            |
| `public_metrics.bookmark_count`              | `bookmark_count`                                    | Flattened                            |
| `public_metrics.impression_count`            | `view_count`                                        | Flattened, renamed                   |
| `author_id` + `includes.users[]`             | `user` (full object inline)                         | Embedded                             |
| Referenced tweets via `includes`             | `quoted_status`, `retweeted_status`                 | Inline objects                       |
| Not available                                | `is_reply`, `is_quote_status`, `is_replies_limited` | Sorsa-only booleans                  |
| Not available                                | `in_reply_to_tweet_id`                              | Sorsa-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:

```bash theme={null}
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:

```bash theme={null}
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:

```json theme={null}
{
  "tweets": [...],
  "next_cursor": "XYZ789"
}
```

When `next_cursor` is missing or `null`, there are no more pages. See [Pagination](https://docs.sorsa.io/pagination) for details.

## HTTP method differences

Some endpoints that are `GET` on the official API are `POST` on Sorsa.

| Action                | Official API           | Sorsa API |
| --------------------- | ---------------------- | --------- |
| Get a tweet           | GET                    | **POST**  |
| Search tweets         | GET                    | **POST**  |
| User timeline         | GET                    | **POST**  |
| Quote tweets          | GET                    | **POST**  |
| Retweeters            | GET                    | **POST**  |
| Replies (comments)    | (no direct equivalent) | **POST**  |
| User profile          | GET                    | GET       |
| Followers / following | GET                    | GET       |
| Lists                 | GET                    | GET       |

Rule of thumb: tweet-content, search, and community endpoints use `POST` with a JSON body, and this includes `/user-tweets` even though it takes a user identifier. User, list, and utility lookups use `GET` with query or path parameters. The one cross-cutting exception is `/check-comment`, which is `GET` despite taking a tweet link. When in doubt, check the endpoint reference.

## Code migration examples

### Get a user profile

**Before (Official API):**

```bash theme={null}
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"
```

```python theme={null}
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"]
```

```javascript theme={null}
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):**

```bash theme={null}
curl "https://api.sorsa.io/v3/info?username=elonmusk" \
  -H "ApiKey: $API_KEY"
```

```python theme={null}
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"]
```

```javascript theme={null}
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:**

```bash theme={null}
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"
```

```python theme={null}
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"])
```

```javascript theme={null}
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:**

```bash theme={null}
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"}'
```

```python theme={null}
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"])
```

```javascript theme={null}
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:**

```python theme={null}
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:**

```bash theme={null}
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
```

```python theme={null}
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
```

```javascript theme={null}
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.

| Operator            | Example                              |
| ------------------- | ------------------------------------ |
| `from:`             | `from:elonmusk`                      |
| `to:`               | `to:elonmusk`                        |
| `since:` / `until:` | `since:2024-01-01 until:2024-02-01`  |
| Hashtag             | `#bitcoin`                           |
| Exact phrase        | `"hello world"`                      |
| `OR`                | `bitcoin OR ethereum`                |
| Exclusion           | `-is:retweet`                        |
| Combined            | `from:elonmusk #bitcoin -is:retweet` |

Full reference: [Search operators](https://docs.sorsa.io/search-operators).

The `/mentions` endpoint additionally supports server-side filters: `min_likes`, `min_replies`, `min_retweets`, `since_date`, `until_date`. See [Track mentions](https://docs.sorsa.io/search-mentions).

## Error handling

The official API returns errors as a structured `errors` array:

```json theme={null}
{
  "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:

```json theme={null}
{ "message": "Tweet not found" }
```

Status codes are consistent across all endpoints: `400`, `401`, `403`, `404`, `429`, `500`. See [Error codes](https://docs.sorsa.io/error-codes).

Rate limit handling: on `429`, back off and retry. The universal limit is **20 req/s** across all endpoints. There are no per-endpoint windows to track. See [Rate limits](https://docs.sorsa.io/rate-limits).

A retry wrapper that handles both APIs:

```python theme={null}
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 (`name` to `display_name`, `text` to `full_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](https://api.sorsa.io/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

| Feature                                     | Endpoint                                            |
| ------------------------------------------- | --------------------------------------------------- |
| Tweet timeline without 3,200-tweet cap      | `POST /user-tweets`                                 |
| Single-call follow check                    | `POST /check-follow`                                |
| Single-call retweet check                   | `POST /check-retweet`                               |
| Single-call comment check                   | `GET /check-comment`                                |
| Single-call quote/retweet check             | `POST /check-quoted`                                |
| Community membership check                  | `POST /check-community-member`                      |
| Community members and feed                  | `POST /community-members`, `POST /community-tweets` |
| In-community search                         | `POST /community-search-tweets`                     |
| Long-form X Article content                 | `POST /article`                                     |
| Verified-only follower filter               | `GET /verified-followers`                           |
| Account country and username change history | `GET /about`                                        |
| Influence score                             | `GET /score`, `GET /score-changes`                  |
| Top followers and following by influence    | `GET /top-followers`, `GET /top-following`          |
| Categorized follower breakdown              | `GET /followers-stats`                              |

## Related references

* [Authentication](https://docs.sorsa.io/authentication)
* [Base URL and versioning](https://docs.sorsa.io/base-url-and-versioning)
* [Rate limits](https://docs.sorsa.io/rate-limits)
* [Pagination](https://docs.sorsa.io/pagination)
* [Error codes](https://docs.sorsa.io/error-codes)
* [Response format](https://docs.sorsa.io/response-format)
* [Search operators](https://docs.sorsa.io/search-operators)
* [Optimizing API usage](https://docs.sorsa.io/optimizing-api-usage)
* [API Reference](https://docs.sorsa.io/api-reference-guide)
