核心要点: 从官方 Twitter/X API 迁移到第三方 REST API 涉及四处改动:把 OAuth 换成单个 API 密钥请求头、更换基础 URL 并重新映射接口路径、把响应解析扁平化,以及把分页切换到一个游标字段。一次读取路径迁移通常需要一到三天。

作者:Sorsa 编辑部 · 更新于 2026 年 7 月 4 日:新增免费起步路径(100 次免费请求,无需绑卡),把价格改用每千次为单位,并对照当前 Sorsa v3 的 40 个接口重新核实了接口映射。

这是给已经决定离开、需要真正迁移方案的团队的技术详解:认证、接口映射、响应解析、分页、HTTP 方法和可用代码。全文以 Sorsa API(一个我们自己开发并运营的 Twitter/X API 替代方案)作为迁移目标,因为从官方 v2 接口过来的映射是现有方案里最干净的之一。把 OAuth 换成一个 ApiKey 请求头、按请求计费(一次批量调用返回最多 100 条推文或资料)、所有套餐统一 20 次/秒,加上无需等待的开发者账号审批,正是一次读取路径迁移落在几天而不是几周的实际原因。读取从每 1,000 条推文 $0.02、每 1,000 份资料 $0.01 起;注册即送 100 次免费请求(无需绑卡、一次性),足够在投入之前跑完一整轮并行对比测试。如果你还在挑服务商,先看我们的 Twitter/X API 替代方案对比;本指南假设那个决定已经做完。

我们自 2022 年起为团队跑过这类迁移,累计处理请求超 50 亿次,所以下面的步骤遵循一份固定清单,而不是泛泛而谈。原则适用于任何只读 REST 服务商;接口名称、响应字段和代码则是 Sorsa 专有的。

目录


从 Twitter/X API 迁移涉及什么? {#what-does-migrating-from-the-twitterx-api-involve}

从官方 Twitter/X API 迁移到按请求计费的 REST API,整体上是做减法:移除 OAuth、删掉字段选择字符串、丢掉响应外壳,再重新映射几个接口路径。核心工作触及认证、接口路径、响应解析和分页,中等规模的读取路径代码库需要一到三天。

在讲分步细节之前,先看完整变更集一览:

  • Authorization: Bearer ... 换成一个 ApiKey 请求头。
  • 把基础 URL 从 https://api.x.com/2 换成 https://api.sorsa.io/v3
  • 重新映射接口路径(表在第 2 步)。
  • 把推文和搜索接口从 GET 换成 POST(第 5 步)。
  • 删除 tweet.fieldsuser.fieldsexpansions:每个字段都默认返回。
  • 扁平化解析器:移除 dataincludesmeta 外壳。
  • 重命名字段:namedisplay_nametextfull_text,指标移到顶层。
  • pagination_tokennext_token 换成 next_cursor
  • 简化错误处理:错误以单个 message 字段返回。

下表映射了与迁移相关的差异。这是这里唯一要紧的对比,因为它决定了你有多少代码要改。

维度官方 X API v2Sorsa API
认证OAuth 2.0 bearer(用户上下文用 1.0a)单个 ApiKey 请求头
基础 URLhttps://api.x.com/2https://api.sorsa.io/v3
字段选择tweet.fieldsuser.fieldsexpansions默认返回所有字段
响应结构data / includes / meta 外壳扁平对象,作者内联
分页pagination_token / next_tokennext_cursor
HTTP 方法(推文、搜索)GETPOST
批量有限每次调用最多 100 条推文或资料
速率限制按接口,15 分钟窗口所有套餐统一 20 次/秒
计费单位按抓取的资源按请求
写操作发帖、私信(关注、点赞、引用仅限 Enterprise)只读

除一处外,每一行都是简化:官方 API 能向 X 写入,只读 API 不能。如果你发帖、发私信或投广告,那条路径仍留在官方 API 上。对读取公开数据来说,这次更换移除了 OAuth、字段选择层和逐接口的速率窗口,那就是迁移的绝大部分。


第 1 步:把 OAuth 换成一个 API 密钥 {#step-1-replace-oauth-with-an-api-key}

认证是删代码最多的改动。官方 API 对仅应用请求用 OAuth 2.0 bearer 令牌,对任何需要用户上下文的东西用 OAuth 1.0a(消费者密钥、access token、每次请求的 HMAC 签名)。只读服务商用请求头里的一个密钥把这一整套替换掉。

之前,在官方 API 上:

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

之后:

bash
curl -X GET "https://api.sorsa.io/v3/info?username=elonmusk" \
  -H "ApiKey: YOUR_API_KEY"

没有令牌刷新、没有签名生成、没有回调 URL。生成一次密钥、存进一个环境变量,此后每个请求都带同一个 ApiKey 请求头。完整参考见认证文档


第 2 步:更换基础 URL 并重新映射接口 {#step-2-swap-the-base-url-and-remap-endpoints}

每个官方 v2 路径都映射到一个新路径。多数调用只改 URL,而推文接口还要改 HTTP 方法。

基础 URL 变成 https://api.sorsa.io/v3。用户接口这样映射:

动作官方 X API v2Sorsa API v3
按用户名取用户GET /2/users/by/username/:usernameGET /info?username=:username
按 ID 取用户GET /2/users/:idGET /info?user_id=:id
多个用户GET /2/users?ids=...GET /info-batch?usernames=...
粉丝GET /2/users/:id/followersGET /followers?user_id=:id
关注GET /2/users/:id/followingGET /follows?user_id=:id
已认证粉丝不可用GET /verified-followers?user_id=:id
账号 “about” 元数据不可用GET /about?username=:username

GET /info-batch 每次调用接收最多 100 个用户名或 ID。GET /followersGET /follows 每页返回最多 200 份完整资料,而官方接口返回的是 ID,之后你还要用第二次调用去补全。

推文:

动作官方 X API v2Sorsa API v3
单条推文GET /2/tweets/:idPOST /tweet-info
多条推文GET /2/tweets?ids=...POST /tweet-info-bulk
用户时间线GET /2/users/:id/tweetsPOST /user-tweets
引用推文GET /2/tweets/:id/quote_tweetsPOST /quotes
转发用户GET /2/tweets/:id/retweeted_byPOST /retweeters
回复(评论)无专用接口POST /comments
长文文章不可用POST /article

POST /tweet-info-bulk 一次请求返回最多 100 条推文,循环调用 /tweet-info 则会花掉 100 次。POST /user-tweets 没有 3,200 条推文上限:用 next_cursor 一路分页到账号的第一条帖子。请求体字段 tweet_link 接受完整 URL 或纯数字 ID。削减请求数的模式,见 API 用量优化指南

搜索:

动作官方 X API v2Sorsa API v3
近期或全量存档搜索GET /2/tweets/search/recentPOST /search-tweets
提及GET .../search/recent?query=@userPOST /mentions
搜索用户不可用POST /search-users

POST /search-tweets 在同一个接口里覆盖历史搜索,而 POST /mentions 加了官方 API 不暴露的互动筛选:min_likesmin_repliesmin_retweetssince_dateuntil_date

列表、社区、验证接口和分析也都有映射,其中好几个没有官方 API 对应项。列表用 GET /list-membersGET /list-followersGET /list-tweets。社区(官方 API 根本不暴露)用 POST /community-membersPOST /community-tweetsPOST /community-search-tweets。单次调用的验证接口(POST /check-followGET /check-commentPOST /check-quotedPOST /check-retweetPOST /check-community-member)一次请求回答一个是/否问题,否则就得去爬完整的粉丝列表或转发用户列表。完整的逐路径表见接口映射参考


第 3 步:扁平化响应解析并重映射字段 {#step-3-flatten-response-parsing-and-remap-fields}

这一步在认证之后触及的代码最多。官方 API 把一个响应拆成 dataincludesmeta。只读服务商返回一个扁平对象、作者内嵌在每条推文里,所以那套“用户拼接”逻辑消失了。

一份用户资料,之前:

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

之后:

json
{
  "id": "44196397",
  "username": "elonmusk",
  "display_name": "Elon Musk",
  "followers_count": 100000000,
  "followings_count": 500,
  "tweets_count": 30000,
  "verified": false,
  "created_at": "2009-06-02T20:12:29Z"
}

字段重命名很小,但测试时容易漏。映射一次,其余就顺了:

官方 X API v2Sorsa API备注
namedisplay_name重命名
textfull_text重命名
public_metrics.followers_countfollowers_count已扁平化
public_metrics.following_countfollowings_count已扁平化,多一个 “s”
public_metrics.tweet_counttweets_count已扁平化并重命名
public_metrics.like_countlikes_count已扁平化,多一个 “s”
public_metrics.retweet_countretweet_count已扁平化,无 “s”
public_metrics.impression_countview_count已扁平化并重命名
conversation_idconversation_id_str重命名
in_reply_to_user_idin_reply_to_username用户名,而非 ID
author_idincludes.users[]user(完整对象内联)内嵌
通过 includes 引用的推文quoted_statusretweeted_status内联

有一处不一致值得点出来,免得浪费你的调试时间:likes 和 follows 是复数(likes_countfollowings_count),而 retweet_countreply_countquote_count 保持单数。作者资料在每个推文响应里都位于 user 之下,所以你在官方 API 上维护的那张 includes.users 查找表可以直接删掉。


第 4 步:把分页切换到游标 {#step-4-switch-pagination-to-a-cursor}

分页收缩成一个字段。官方 API 在查询里用 pagination_token、返回 meta.next_token;只读服务商在响应顶层用 next_cursor

对 GET 接口,把 next_cursor 作为查询参数传;对 POST 接口,把它放进 JSON 请求体:

bash
curl -X POST "https://api.sorsa.io/v3/search-tweets" \
  -H "ApiKey: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "query": "from:elonmusk", "next_cursor": "ABC123" }'

响应是扁平的,next_cursor 缺失或为 null 就说明你已经到达最后一页:

json
{ "tweets": [], "next_cursor": "XYZ789" }

完整模式,包括粉丝列表分页,见游标分页文档


第 5 步:在需要处把 GET 换成 POST {#step-5-switch-get-to-post-where-required}

这是团队容易忘、然后调试十分钟的改动。在官方 API 上,推文和搜索读取是 GET。在这个 REST API 上,任何接收推文标识符或搜索查询的都变成 POST,而任何接收用户标识符或列表 ID 的仍是 GET。

动作官方 APISorsa API
获取一条推文GETPOST
搜索推文GETPOST
用户时间线GETPOST
引用推文、转发用户GETPOST
回复(评论)不适用POST
用户资料GETGET
粉丝、关注GETGET
列表GETGET

经验法则:推文标识符或搜索查询意味着 POST;用户标识符或列表 ID 意味着 GET。


第 6 步:迁移代码 {#step-6-migrate-the-code}

下面是三个有代表性的迁移,分别用 curl、Python 和 JavaScript 呈现。这些是你在读取路径代码库里会反复用到的模式。

获取一份用户资料

之前:

python
import requests

r = requests.get(
    "https://api.x.com/2/users/by/username/elonmusk",
    params={"user.fields": "public_metrics,verified,created_at"},
    headers={"Authorization": f"Bearer {BEARER_TOKEN}"},
)
user = r.json()["data"]
followers = user["public_metrics"]["followers_count"]
name = user["name"]

之后:

python
import requests

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

字段选择字符串没了,指标也在顶层。

搜索推文

之前,作者拼接是必需的:

javascript
const params = new URLSearchParams({
  query: "from:elonmusk since:2024-01-01",
  "tweet.fields": "created_at,public_metrics",
  expansions: "author_id",
  "user.fields": "username,name",
});
const res = await fetch(`https://api.x.com/2/tweets/search/recent?${params}`, {
  headers: { Authorization: `Bearer ${BEARER_TOKEN}` },
});
const data = await res.json();
const users = Object.fromEntries((data.includes?.users || []).map(u => [u.id, u]));
for (const t of data.data || []) {
  console.log(t.text, "by", users[t.author_id].username);
}

之后,每条推文已经带着作者:

javascript
const res = await fetch("https://api.sorsa.io/v3/search-tweets", {
  method: "POST",
  headers: { ApiKey: API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({ query: "from:elonmusk since:2024-01-01" }),
});
const data = await res.json();
for (const t of data.tweets) {
  console.log(t.full_text, "by", t.user.username);
}

用户拼接表消失了,因为作者内嵌在每条推文里。

分页遍历每一个粉丝

python
def fetch_all_followers(user_id, api_key):
    url = "https://api.sorsa.io/v3/followers"
    headers = {"ApiKey": api_key}
    followers, next_cursor = [], None
    while True:
        params = {"user_id": user_id}
        if next_cursor:
            params["next_cursor"] = next_cursor
        data = requests.get(url, headers=headers, params=params).json()
        followers.extend(data.get("users", []))
        next_cursor = data.get("next_cursor")
        if not next_cursor:
            break
    return followers

每页返回最多 200 份完全补全的资料,所以一次在官方 API 上需要“一次 ID 调用加一次补全调用”的粉丝关系图拉取,在这里一遍就走完。按语言的细节,我们的 Python 版 Twitter API 指南覆盖了完整的读取路径。


第 7 步:保留你的搜索查询并处理错误 {#step-7-keep-your-search-queries-and-handle-errors}

你的搜索查询原样迁移。支持相同 Twitter 高级搜索操作符的服务商,读取 from:to:since:until:、带引号短语、话题标签、OR-is:retweet 的方式,与官方近期搜索接口完全一致。现有的查询字符串因此不用改。

把现有查询字符串照搬过来;高级搜索语法参考列出了完整集合。mentions 接口还暴露了互动筛选(min_likesmin_repliesmin_retweetssince_dateuntil_date),所以你在官方 API 上写的任何客户端“按互动下限过滤”逻辑,都可以搬到服务端。

错误处理也简化了。官方 API 返回一个结构化错误数组,只读服务商则返回一个单一的 message 字段,配标准状态码:400、401、403、404、429 和 500。对 429,策略是等一秒再重试,因为限制是所有接口和套餐统一 20 次/秒,没有逐接口的窗口要追踪。一个防御性的重试封装:

python
import time, requests

def call_with_retry(method, url, max_retries=3, **kwargs):
    for attempt in range(max_retries):
        r = requests.request(method, url, **kwargs)
        if r.status_code == 429:
            time.sleep(2 ** attempt)
            continue
        r.raise_for_status()
        return r.json()
    raise RuntimeError(f"failed after {max_retries} retries")

完整的状态码列表见错误码参考


迁移清单 {#migration-checklist}

把这个当作一次读取路径迁移的工作清单:

  1. 到处把 Authorization: Bearer ... 换成 ApiKey 请求头。
  2. 移除 OAuth 1.0a 签名逻辑(消费者密钥、access token、签名)。
  3. 把基础 URL 更新为 https://api.sorsa.io/v3
  4. 用第 2 步的表重映射每一个接口路径。
  5. 为推文、搜索、评论、引用和转发用户接口把 GET 换成 POST。
  6. 删除 tweet.fieldsuser.fieldsexpansions
  7. 移除 data / includes / meta 的拆包。
  8. 在你的模型里重命名字段(nametextpublic_metrics 块)。
  9. pagination_tokennext_token 换成 next_cursor
  10. 为单一 message 字段的形态更新错误处理。
  11. 把速率限制逻辑设为统一 20 次/秒,无逐接口窗口。
  12. 部署前在在线试用工具 Playground 里测试关键接口。
  13. key-usage-info 接口追踪额度消耗(见密钥用量参考)。
  14. 如果你也向 X 写入,就保留官方密钥;只迁移读取路径。

实战:一个学术团队的存档迁移 {#in-practice-an-academic-groups-archival-migration}

一个学术研究团队在 2025 年年中找到我们,当时在官方 API 上跑一条为纵向研究采集推文的数据管道。有两个问题卡住了他们:用户时间线接口每个账号封顶在最近 3,200 条推文,弄坏了他们的历史覆盖;而粉丝接口返回的是裸 ID,还要第二次查询才能变成可用的资料。

读取路径迁移由一名研究者花了约两天。POST /user-tweets 移除了 3,200 条推文的上限,用 next_cursor 一路分页回每个账号的第一条帖子,存档缺口就补上了。粉丝拉取也收缩成一遍走完,因为 GET /followers 每页返回最多 200 份完整资料,这大致把那部分工作的请求数减半。唯一真正的摩擦是字段重命名(namedisplay_nametextfull_text,以及复数化的 likes_count),在几小时的测试里、而不是设计阶段被抓出来。具体数字因负载而异,迁移的形状不会变。


常见问题 {#frequently-asked-questions}

能一个接口一个接口地从 Twitter/X API 迁移吗?

能,而且渐进式迁移通常是更稳妥的路径。在你的数据调用前放一个薄抽象层,把一个接口指向新服务商,对照官方 API 校验它的输出几天,再迁下一个。应用代码永远不必在一次大重写里改动,而且哪里看着不对,你可以独立回滚某个接口。

如何在不弄坏生产的情况下测试 Twitter/X API 迁移?

切换之前,并行跑两个 API 并对比解析后的输出。最轻的选项是在线试用工具 Playground,它发请求而无需集成代码。更强的选项是一个并排脚本,同时调用两个 API 并比较结果;最彻底的是一个功能开关,把一定比例的流量路由到新服务商,让你能立即回滚。

迁移后 Twitter 高级搜索查询还能用吗?

能,只要服务商支持相同的高级搜索操作符。Sorsa 的 search-tweets 和 mentions 接口读取 from:、to:、since:、until:、带引号短语、话题标签、OR 和 -is:retweet 的方式,与官方近期搜索接口完全一致,所以现有查询字符串不用改就能迁移。mentions 接口还加了官方 API 不暴露的互动筛选,比如 min_likes 和 min_retweets。

迁移后如何处理速率限制和错误?

错误处理变得更简单。Sorsa 把每个错误以单一 message 字段返回,而不是官方 API 的结构化错误数组,配标准状态码(400、401、403、404、429 和 500)。速率限制是所有套餐统一 20 次/秒,没有逐接口的 15 分钟窗口。遇到 429,等一秒再重试,没有 reset 头要追踪。

迁移会移除 3,200 条推文的时间线限制吗?

会。官方 v2 用户时间线接口每个账号封顶在最近 3,200 条推文。Sorsa 的 POST /user-tweets 没有这种上限:用 next_cursor 一直分页,直到响应不再返回它,你就到了账号的第一条帖子。做存档、情感历史和训练数据工作时,这往往正是迁移发生的原因。

如果替代方案是只读的,如何继续向 X 写入?

写操作保留官方 X API,只迁移读取路径。发帖、回复、私信、点赞、关注和广告全都留在官方 API 上,它是唯一能代表用户行事的系统。多数团队最终采用混合方案:一小笔官方 API 预算用于写入,一个按请求计费的读取 API 用于高量数据拉取。Sorsa 从设计上就是只读,这也顺带移除了一类写权限和账号封禁风险。


我们如何核实本指南 {#how-we-verified-this-guide}

本详解取材于我们自 2022 年起运营 X API 替代方案的一线工作,以及我们为离开官方平台的团队跑过的读取路径迁移。接口路径、参数名、响应字段和 ApiKey 请求头对照线上的 Sorsa API 文档核实;官方 X API 的认证模型、响应外壳和按量付费费率则对照 X 的开发者文档和定价页面核查,反映了 2026 年 4 月 20 日的变化。这个决定的成本面,见我们的当前 X API 价格详解。每个代码示例都按所示可运行。于 2026 年 6 月 13 日核实。

审校:Keksich(Sorsa 创始人,X API 研究者)


如何开始 {#getting-started}

如果你的数据管道读取推文、资料、粉丝或搜索结果,给迁移划定范围最快的办法,是跑几次调用、把响应结构和你当前的解析器比一比。Sorsa API 快速上手在拿到密钥之后几分钟内让你完成第一次请求,历史数据文档则展示了时间线接口如何在没有 3,200 条推文上限的情况下回溯到 2006 年。注册即送 100 次免费请求,一次性、无需绑卡、永不过期,足够在付费之前跑完一整轮并行对比测试。之后,读取从每 1,000 条推文 $0.02、每 1,000 份资料 $0.01 起,套餐 $49/月含 1 万次请求起,每档都统一 20 次/秒,而且你和第一次调用之间没有开发者账号审批。先把一个接口放在开关后面指过去、对比输出,再迁移其余的。