# Migrate from SocialData to TwittrAPI

> Move a SocialData integration to TwittrAPI: endpoint mapping, auth header, cursor pagination, the legacy fields you already know, and before/after code in Python and Node.js.

HTML page: https://www.twittrapi.com/guides/migrate-from-socialdata
Competitor facts checked on 2026-10-10.

This guide moves a SocialData read integration to TwittrAPI. SocialData paths and parameters come from its public docs as of October 2026. Ours are checked against our [endpoint reference](https://www.twittrapi.com/docs/reference); every TwittrAPI path below links to its reference page. If you have not decided yet, the [comparison](https://www.twittrapi.com/compare/socialdata) covers price, limits and what each side does better.

## What changes

| | SocialData | TwittrAPI |
| --- | --- | --- |
| Base URL | `https://api.socialdata.tools` | `https://api.twittrapi.com` |
| Auth header | `Authorization: Bearer <key>` | `X-API-KEY: <key>` |
| Pagination in | `cursor` | `cursor` |
| Pagination out | `next_cursor` | Bottom cursor inside X's JSON (`cursorType: "Bottom"`) |
| Page size | about 20 tweets per page (as of October 2026), fixed | `count`, default 20; X decides the final number |
| Response | Flat snake_case objects | X's own JSON; tweet fields sit in `legacy` |
| Errors | `status` and `message`; 402, 422, 500 | `{"error": "..."}` with a non-2xx status |
| Rate limit | 120 requests per minute per user (as of October 2026) | 30 requests per second per key after the first top-up |
| Billing | Per tweet or profile returned | One call per request |

## Endpoint mapping

| SocialData | TwittrAPI | Notes |
| --- | --- | --- |
| `GET /twitter/tweets/{id}` | [`GET /simpletweet/{tweetId}`](https://www.twittrapi.com/docs/reference/simpletweet-tweetid) | |
| `GET /twitter/tweets/{id}/comments` | [`GET /tweet/{tweetId}`](https://www.twittrapi.com/docs/reference/tweet-tweetid) | Tweet plus replies. `rankingMode`: Relevance, Recency or Likes. About 20 replies per page. |
| `GET /twitter/tweets/{id}/quotes` | [`GET /tweet/{tweetId}/quotes`](https://www.twittrapi.com/docs/reference/tweet-tweetid-quotes) | |
| `GET /twitter/tweets/{tweet_id}/retweeted_by` | [`GET /tweet/{tweetId}/retweets`](https://www.twittrapi.com/docs/reference/tweet-tweetid-retweets) | |
| `GET /twitter/user/{username}` | [`GET /user/by/username/{username}`](https://www.twittrapi.com/docs/reference/user-by-username-username) | By ID: [`GET /user/{userId}`](https://www.twittrapi.com/docs/reference/user-userid). |
| `POST /twitter/users-by-ids` | [`GET /user/users/by/ids?ids=`](https://www.twittrapi.com/docs/reference/user-users-by-ids) | Up to 100 IDs, one call. |
| `GET /twitter/user/{user_id}/tweets` | [`GET /user/{userId}/tweets`](https://www.twittrapi.com/docs/reference/user-userid-tweets) | |
| `GET /twitter/user/{user_id}/tweets-and-replies` | [`GET /user/{userId}/tweetsAndReplies`](https://www.twittrapi.com/docs/reference/user-userid-tweetsandreplies) | |
| `GET /twitter/search?query=&type=Latest` | [`GET /search?searchItem=&category=Latest`](https://www.twittrapi.com/docs/reference/search) | `query` also works as the parameter name. Categories: Top, Latest, People, Photos, Videos. |
| `GET /twitter/user/{user_id}/followers` | [`GET /user/{userId}/followers`](https://www.twittrapi.com/docs/reference/user-userid-followers) | |
| `GET /twitter/user/{user_id}/following` | [`GET /user/{userId}/followings`](https://www.twittrapi.com/docs/reference/user-userid-followings) | `/following` is an alias. |
| Verified followers | [`GET /user/{userId}/verifiedFollowers`](https://www.twittrapi.com/docs/reference/user-userid-verifiedfollowers) | |
| `GET /twitter/list/{list_id}/tweets` | [`GET /lists/tweets?listId=`](https://www.twittrapi.com/docs/reference/lists-tweets) | |
| `GET /twitter/list/{list_id}/members` | [`GET /lists/members?listId=`](https://www.twittrapi.com/docs/reference/lists-members) | |
| `GET /twitter/community/{community_id}/tweets` | [`GET /communities/{communityId}/tweets`](https://www.twittrapi.com/docs/reference/communities-communityid-tweets) | |
| Community details | [`GET /communities/{communityId}`](https://www.twittrapi.com/docs/reference/communities-communityid) | |
| Community members | [`GET /communities/{communityId}/members`](https://www.twittrapi.com/docs/reference/communities-communityid-members) | |
| Community search | [`GET /communities/search/{searchTerm}`](https://www.twittrapi.com/docs/reference/communities-search-searchterm) | |
| Mentions | [`GET /search?searchItem=@jack&category=Latest`](https://www.twittrapi.com/docs/reference/search) | No dedicated endpoint; use search. |
| `GET /twitter/space/{space_id}` | not available | |
| Thread, highlights, affiliates, similar profiles, extended bio | not available | |
| Verify following | [`GET /user/friendships/show`](https://www.twittrapi.com/docs/reference/user-friendships-show) | |
| Verify retweeted / commented | not available | Scan [`GET /tweet/{tweetId}/retweets`](https://www.twittrapi.com/docs/reference/tweet-tweetid-retweets) or the replies yourself. |
| Monitoring and webhooks | not available | Poll search or timelines. |
| (none) | [`GET /trends/{locationId}`](https://www.twittrapi.com/docs/reference/trends-locationid), [`GET /misc/locations`](https://www.twittrapi.com/docs/reference/misc-locations) | Trends by WOEID. |

Rows without a path on the SocialData side are endpoints whose exact path we did not copy from its docs.

## Response shape: same names, different nesting

SocialData flattens a tweet into one object with the author in `user`. TwittrAPI passes X's JSON through, where a tweet looks roughly like this:

```json
{
  "rest_id": "1790000000000000000",
  "core": { "user_results": { "result": { "rest_id": "12", "core": { "screen_name": "jack" }, "legacy": { "followers_count": 1 } } } },
  "legacy": { "full_text": "...", "favorite_count": 10, "retweet_count": 2, "reply_count": 1, "quote_count": 0, "created_at": "...", "id_str": "1790000000000000000" },
  "views": { "count": "1234" }
}
```

The `legacy` object uses the same names SocialData uses. The author's handle moved from `legacy.screen_name` to `core.screen_name` in 2025, so check both. Long tweets keep their full text in `note_tweet.note_tweet_results.result.text`. Timelines wrap tweets in `instructions` and entries; the [response format docs](https://www.twittrapi.com/docs/response-format) explain the structure.

## Before and after: Python

Before:

```python
import os, requests

r = requests.get(
    "https://api.socialdata.tools/twitter/search",
    headers={"Authorization": f"Bearer {os.environ['SOCIALDATA_KEY']}"},
    params={"query": "from:jack lang:en", "type": "Latest"},
)
data = r.json()
for t in data["tweets"]:
    print(t["id_str"], t["user"]["screen_name"], t["favorite_count"], t["full_text"])
cursor = data.get("next_cursor")
```

After, with the [Python SDK](https://pypi.org/project/twittrapi/) and the extractor from our [examples repository](https://github.com/twittrapi/examples):

```python
from twittrapi import TwittrAPI, extract_bottom_cursor
from extract import tweets  # python/_shared/extract.py from the examples repo

client = TwittrAPI()  # reads TWITTRAPI_API_KEY

page = client.search.tweets("from:jack lang:en", category="Latest")
for t in tweets(page):
    print(t["id"], t["author"], t["likes"], t["text"])
cursor = extract_bottom_cursor(page)
```

## Before and after: Node.js

Before:

```js
const res = await fetch("https://api.socialdata.tools/twitter/user/12/followers", {
  headers: { Authorization: `Bearer ${process.env.SOCIALDATA_KEY}` },
});
const { users, next_cursor } = await res.json();
```

After, with the [Node SDK](https://www.npmjs.com/package/twittrapi):

```js
import { TwittrAPI } from "twittrapi";
import { extractUsers } from "./extract.mjs"; // node/_shared/extract.mjs from the examples repo

const client = new TwittrAPI(); // reads TWITTRAPI_API_KEY

for await (const page of client.paginate(client.users.followers, { userId: "12", count: 100 }, { maxPages: 5 })) {
  const users = extractUsers(page.data);
  // page.nextCursor is the cursor for the next call
}
```

## Keep your data model: an adapter

If your code reads SocialData's tweet objects, build the same shape from X's JSON in one function:

```python
def to_socialdata_shape(t: dict) -> dict:
    """t is a raw tweet object (tweet_results.result) from X's JSON."""
    legacy = t.get("legacy") or {}
    author = ((t.get("core") or {}).get("user_results") or {}).get("result") or {}
    a_legacy = author.get("legacy") or {}
    a_core = author.get("core") or {}
    note = (((t.get("note_tweet") or {}).get("note_tweet_results") or {}).get("result") or {})
    return {
        "id_str": t.get("rest_id"),
        "full_text": note.get("text") or legacy.get("full_text"),
        "tweet_created_at": legacy.get("created_at"),
        "favorite_count": legacy.get("favorite_count"),
        "retweet_count": legacy.get("retweet_count"),
        "reply_count": legacy.get("reply_count"),
        "quote_count": legacy.get("quote_count"),
        "bookmark_count": legacy.get("bookmark_count"),
        "views_count": int((t.get("views") or {}).get("count") or 0) or None,
        "lang": legacy.get("lang"),
        "user": {
            "id_str": author.get("rest_id"),
            "screen_name": a_core.get("screen_name") or a_legacy.get("screen_name"),
            "name": a_core.get("name") or a_legacy.get("name"),
            "followers_count": a_legacy.get("followers_count"),
            "friends_count": a_legacy.get("friends_count"),
        },
    }
```

Note one difference in dates: SocialData's `tweet_created_at` is its own field, while X's `legacy.created_at` uses X's format (`Wed Oct 10 20:19:24 +0000 2018`). Parse it if you need ISO dates.

## Pagination differences

Figure: paging through followers with a cursor. Call /user/{userId}/followers, pass the returned cursor back as `cursor` for the next page, and stop at the last page. Every page is one call ($0.065 to $0.09 per 1,000 calls).

- Look for the bottom cursor in the response instead of `next_cursor`. The SDKs' `extract_bottom_cursor` / `extractBottomCursor` do this; see [Pagination](https://www.twittrapi.com/docs/helpful-infos/cursor).
- Stop when there is no bottom cursor, it repeats, or the page holds no items.
- Set `count` on timelines and follower lists. X decides how many items it returns.
- Do not reuse SocialData cursors.

## Billing differences to plan for

- Every successful call is billed, including empty pages. Calls that end in 400, 408, 409 or 5xx are refunded. Responses carry `X-Price-Per-Call`, `X-Remaining-Balance` and `X-Refunded` headers. See [Pricing and billing](https://www.twittrapi.com/docs/billing).
- A page of results is one call, so large pages are cheaper per item.
- 429 (over your per-second limit) is not charged. Back off and retry; the SDKs do this for you.

## Checklist

- List the SocialData endpoints you call and how often.
- Check each against the mapping; plan around Spaces, threads, verification checks and webhooks if you use them.
- Create an account at [/start](https://www.twittrapi.com/start) and set `TWITTRAPI_API_KEY`.
- Change auth from `Authorization: Bearer` to `X-API-KEY`, or install the SDK.
- Replace `POST /twitter/users-by-ids` with `GET /user/users/by/ids`.
- Write the adapter to your current tweet and user shape; test it on saved responses.
- Switch pagination to bottom cursors.
- If you poll, check how often your calls return nothing, and set the interval with per-call billing in mind.
- Run both side by side on a sample, compare items and cost, then move the rest.

TwittrAPI is not affiliated with X Corp. or SocialData. Reading data needs no X account; optional [account actions](https://www.twittrapi.com/docs/account-actions) use your own X session cookie and proxy.

## Frequently asked questions

### Will my SocialData field names still work?

Mostly, one level down. SocialData's tweet fields (full_text, favorite_count, retweet_count, reply_count, quote_count, id_str, created_at) match the field names inside the legacy object of each tweet in X's JSON, which TwittrAPI passes through. The nesting is different, so you need a small adapter.

### Does TwittrAPI charge for empty results?

Yes. A successful call is billed even if X returns no items. On SocialData's per-item endpoints an empty page costs nothing. If you poll often for new tweets, account for that.

### What replaces POST /twitter/users-by-ids?

[`GET /user/users/by/ids?ids=1,2,3`](https://www.twittrapi.com/docs/reference/user-users-by-ids), up to 100 IDs per call, billed as one call.

### Is there a trends endpoint?

Yes. [`GET /trends/{locationId}`](https://www.twittrapi.com/docs/reference/trends-locationid) returns X's trends for a location, and [`GET /misc/locations`](https://www.twittrapi.com/docs/reference/misc-locations) lists the locations.


## Sources

- [SocialData: search](https://docs.socialdata.tools/reference/get-search-results)
- [SocialData rate limits](https://docs.socialdata.tools/getting-started/rate-limits)
- [SocialData docs index](https://docs.socialdata.tools/llms.txt)
- [SocialData authentication](https://docs.socialdata.tools/getting-started/authentication)
- [SocialData: get tweet](https://docs.socialdata.tools/reference/get-tweet)
- [SocialData: user tweets](https://docs.socialdata.tools/reference/get-user-tweets-replies)
- [SocialData: followers](https://docs.socialdata.tools/reference/get-user-followers)
