跳到正文

怎么从 Twitter/X API 迁移

从官方 X API 迁出只需改五处代码:把每个 v2 接口映射到新路径,用 API 密钥换掉 OAuth,去掉字段和 expansions,重写 JSON 解析,并换掉分页令牌。

  • 更新于
  • 发布于
  • 阅读约 13 分钟

简短回答: 该迁的是读取:官方 X API 每条帖子收费 $0.005,每个用户 $0.010,自 2026 年 11 月 22 日起,随帖子返回的每个作者再加 $0.010。要迁移它们,用 Sorsa API:一个 ApiKey 请求头,搜索每次请求 20 条推文,每 1,000 条 $0.07 起,另有 100 次免费请求用来测试这份对照表。

接口
POST /search-tweets
每页条数
每次请求 20 条推文
价格
每 1,000 条 $0.07 起

#2023 年以来官方 X API 发生了什么变化

为官方 X API 写的代码大多数仍然能跑;2023 年以来变的是它能调用什么、返回什么,以及每个响应要花多少钱。下面这份带日期的清单来自 X 的更新日志(在新标签页打开)和它的开发者论坛通知,并标出了每项变化影响集成的哪一部分。

日期官方 X API 的变化对你的代码有什么影响
2023-03-30付费档取代免费访问;Free 变成只写免费应用上的读取会返回错误
2023-06-26Basic 和 Pro 移除关注类接口这些套餐上的粉丝和关注调用停止工作
2023-09-28v1.1 的 search/tweets 和 users/search 被弃用v1.1 搜索代码需要换新接口
2026-01-19搜索去掉 min_likes、min_replies、min_reposts用到它们的查询必须先去掉,直到 5 月 4 日
2026-02-06按量付费成为模式;额度需预先购买余额归零后调用即被阻止
2026-02-23只有作者召唤你时才允许程序化回复回复机器人会被拒绝
2026-04-20自助档移除关注、点赞和引用发帖这些写入调用停止工作
2026-05-04搜索迁到新索引;关键词结果不再含转发同一个查询返回的帖子变少
2026-06-01 和 2026-09-01旧版 Basic,随后是 Pro 套餐迁到按量付费月度上限变成按资源计费的账单
2026-11-22expansions 开始计费:每个作者 $0.010,每个媒体 $0.005expansions=author_id 能让一条帖子的成本翻三倍
  • 2023-03-30

    官方 X API 的变化
    付费档取代免费访问;Free 变成只写
    对你的代码有什么影响
    免费应用上的读取会返回错误
  • 2023-06-26

    官方 X API 的变化
    Basic 和 Pro 移除关注类接口
    对你的代码有什么影响
    这些套餐上的粉丝和关注调用停止工作
  • 2023-09-28

    官方 X API 的变化
    v1.1 的 search/tweets 和 users/search 被弃用
    对你的代码有什么影响
    v1.1 搜索代码需要换新接口
  • 2026-01-19

    官方 X API 的变化
    搜索去掉 min_likes、min_replies、min_reposts
    对你的代码有什么影响
    用到它们的查询必须先去掉,直到 5 月 4 日
  • 2026-02-06

    官方 X API 的变化
    按量付费成为模式;额度需预先购买
    对你的代码有什么影响
    余额归零后调用即被阻止
  • 2026-02-23

    官方 X API 的变化
    只有作者召唤你时才允许程序化回复
    对你的代码有什么影响
    回复机器人会被拒绝
  • 2026-04-20

    官方 X API 的变化
    自助档移除关注、点赞和引用发帖
    对你的代码有什么影响
    这些写入调用停止工作
  • 2026-05-04

    官方 X API 的变化
    搜索迁到新索引;关键词结果不再含转发
    对你的代码有什么影响
    同一个查询返回的帖子变少
  • 2026-06-01 和 2026-09-01

    官方 X API 的变化
    旧版 Basic,随后是 Pro 套餐迁到按量付费
    对你的代码有什么影响
    月度上限变成按资源计费的账单
  • 2026-11-22

    官方 X API 的变化
    expansions 开始计费:每个作者 $0.010,每个媒体 $0.005
    对你的代码有什么影响
    expansions=author_id 能让一条帖子的成本翻三倍

其中两项改变了结果,却没动你的任何一行代码。自从新搜索索引(在新标签页打开)在 5 月 4 日上线,四月还好好的关键词搜索,现在返回的结果里不再有转发。而从 11 月 22 日起,同样带 expansions=author_id 的请求,原来每 1,000 条帖子 $5.00,现在最高要 $15.00,因为 X 的 expansions 通知(在新标签页打开)把每个返回的作者按一次用户读取计费。

套餐也在运行中的代码底下变了。X 在 2026 年 6 月 1 日之后的切换日期终止了旧版 Basic(在新标签页打开)订阅,并在 9 月 1 日之后终止了旧版 Pro(在新标签页打开);Pro 月付账号在切换时被充入 $5,000 的额度,并开启了自动充值。现在每次读取都从额度里扣,每条帖子 $0.005,每个用户或粉丝 $0.010,每月最多读取 300 万条帖子,完整的费率表在我们的 X API 价格拆解里。

#哪些迁到 Sorsa API,哪些留在 X

公开数据的每一种读取都可以迁:搜索、时间线、帖子和用户查询、粉丝、列表、引用、转发者、回复、Spaces、社区和趋势。Sorsa API 是一款 Twitter(X)API 与爬虫,2022 年起投入生产,累计处理请求超 60 亿次,一个密钥就能返回这一切,不需要 OAuth,也不需要 X 开发者账号,详见不用开发者账号的 Twitter API。它在设计上是只读的,所以写入类的调用还留在原处。

下面这些留在官方 X API:

  • 发帖、回复、删除和媒体上传。
  • 私信,以及点赞、关注、书签和屏蔽,无论是操作还是你自己账号的活动列表。
  • 谁点赞了某条帖子,以及受保护账号的任何内容。
  • Filtered stream、firehose、webhook 和 X Activity API:Sorsa API 是轮询的,不是推送的(轮询的写法见下文)。
  • 帖子计数、Community Notes 和 Ads API。

你的 X 账号不参与这些读取,因为 Sorsa API 从不索要你的登录信息或 Cookie。拆分之后,官方 X API 只对写入按请求计费:每创建一条帖子 $0.015,带链接的帖子 $0.20。

钱主要花在读取上。就搜索和时间线读取而言,100 万条推文在官方 X API 上要 $5,000(还没算作者计费),在 Sorsa API 上 $70 起,而且每条推文自带作者,不另收费。按 ID 查询更进一步:POST /tweet-info-bulk 每次请求返回 100 条推文,每 1,000 条 $0.014 起。

通过搜索读取的推文数

官方 X API

$5,000

读取 100 万 条推文

每条帖子 $0.005,不含作者

Sorsa API

$70 起

读取 100 万 条推文

每 1,000 条 $0.07 起,含作者

#分六步迁移到 Sorsa API

六个步骤能把一套 v2 集成从盘点带到切换。前五步是改你的代码;最后一步要跑几天,期间两套 API 回答同样的读取。

  1. **列出你的代码发出的每一个调用。**在代码里搜索 api.x.com/2、api.twitter.com/2 和你的客户端的方法名,比如 Tweepy 的 search_recent_tweets 或 xdk 的 posts.search_recent。在 X 的开发者控制台导出上个月按资源统计的用量,并把每个调用标记为读取或写入。

  2. 获取密钥,发出一个请求。注册,从控制台复制密钥,然后调用替代你最繁忙读取的那个接口。

    终端
    curl -X POST "https://api.sorsa.io/v3/search-tweets" \
      -H "ApiKey: YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"query": "from:mira_moves_reads", "order": "latest"}'
    
    响应200 OK
    {
      "tweets": [
        {
          "id": "1852331946812457001",
          "full_text": "Read path moved. Writes stay where they were.",
          "created_at": "2026-10-30T14:05:12Z",
          "lang": "en",
          "likes_count": 41,
          "retweet_count": 6,
          "reply_count": 3,
          "quote_count": 1,
          "view_count": 2380,
          "conversation_id_str": "1852331946812457001",
          "is_reply": false,
          "user": {
            "id": "1592041833471827968",
            "username": "mira_moves_reads",
            "display_name": "Mira Moves Reads",
            "followers_count": 912,
            "verified": false
          }
        }
      ],
      "next_cursor": "DAADDAABCgABGbHk7m4WYAEKAAIZsd"
    }
    
  3. **把每个读取映射到对应接口。**下一节的表格和转换器,把每个 v2 读取与它的 Sorsa API 路径和参数配成对。

  4. **用一个请求头换掉 OAuth、字段和 expansions。**在读取调用上,删掉 bearer 令牌或 OAuth 1.0a 签名,改为发送 ApiKey: YOUR_API_KEY。去掉 tweet.fields、user.fields 和 expansions:所有字段和作者默认都会返回。

  5. **重写解析和分页。**读取 data 和 includes 的地方,改读 tweets 或 users;重命名有差异的字段;把 next_cursor 送回去,直到它返回为空。

  6. **影子运行,然后切换。**把你的读取复制一份发给 Sorsa API 跑几天,对比结果,分阶段迁移流量,并把 X 的密钥留给写入。

#把每个 X API v2 接口映射到 Sorsa API

集成可能调用的每个公开读取,在 Sorsa API 里都有一个对应的接口。下表把每个 v2 路径(按 X 的 OpenAPI 规范(在新标签页打开)所写)与它的替代接口配对,并标出一次请求返回多少;Sorsa API 按请求计费,不按条目计费,所以最后一列决定价格。

X API v2 调用Sorsa API 接口一次请求返回
GET /2/tweets/search/recentPOST /search-tweets(在新标签页打开)约 20 条推文
GET /2/tweets/search/allPOST /search-tweets,带 since: 和 until:约 20 条推文
GET /2/tweets/:idPOST /tweet-info(在新标签页打开)1 条推文
GET /2/tweets(按 ID)POST /tweet-info-bulk(在新标签页打开)最多 100 条推文
GET /2/users/:id/tweetsPOST /user-tweets(在新标签页打开)约 20 条推文
GET /2/users/:id/mentionsPOST /mentions(在新标签页打开)约 20 条推文
GET /2/tweets/:id/quote_tweetsPOST /quotes(在新标签页打开)约 20 条推文
GET /2/tweets/:id/retweeted_byPOST /retweeters(在新标签页打开)一页用户资料
用 conversation_id: 搜索POST /comments(在新标签页打开)约 20 条回复
GET /2/users/by/username/:usernameGET /info(在新标签页打开)1 份资料
GET /2/users 和 GET /2/users/byGET /info-batch(在新标签页打开)最多 100 份资料
GET /2/users/:id/followersGET /followers(在新标签页打开)最多 200 份资料
GET /2/users/:id/followingGET /follows(在新标签页打开)最多 200 份资料
GET /2/users/searchPOST /search-users(在新标签页打开)一页用户资料
GET /2/lists/:id/membersGET /list-members(在新标签页打开)一页用户资料
GET /2/lists/:id/followersGET /list-followers(在新标签页打开)一页用户资料
GET /2/lists/:id/tweetsGET /list-tweets(在新标签页打开)约 20 条推文
GET /2/spaces/:idGET /spaces(在新标签页打开)1 个 Space
GET /2/communities/:idPOST /community-info(在新标签页打开)1 个社区
GET /2/communities/searchPOST /search-communities(在新标签页打开)一页社区
GET /2/trends/by/woeid/:woeidGET /trends(在新标签页打开)1 个地区的趋势
GET /2/usage/tweetsGET /key-usage-info(在新标签页打开)你的余额,免费

路径一换,有四个习惯也要跟着变。推文接口用 POST 加 JSON 请求体,而用户、列表和工具类查询仍然是 GET。凡是 v2 要数字 ID 的地方,都可以直接用用户名,所以每次调用时间线前多出来的那次查询可以去掉。tweet_link 接受完整的帖子 URL,也接受单独的 ID。另外,POST /user-tweets 能一直翻过最新的 3,200 条帖子,而 X 的用户时间线到那里就停了。

有些 v2 读取没有对应项,因为 Sorsa API 只返回任何人在公开主页上都能看到的内容:点赞的用户、一个用户的点赞和书签、帖子计数、首页时间线,以及所有数据流。Sorsa API 还能回答 v2 回答不了的问题:POST /check-follow(在新标签页打开) 一次请求就能告诉你一个账号是否关注了另一个账号,GET /about(在新标签页打开) 返回一个账号的所在国家和用户名历史。

下面的转换器每次把一个 v2 调用转成对应的 Sorsa API 请求,列出变化的字段,以及两边各 1,000 条结果的价格。

之前:官方 X API v2
curl -G "https://api.x.com/2/tweets/search/recent" \
  -H "Authorization: Bearer $X_BEARER_TOKEN" \
  --data-urlencode "query=from:mira_moves_reads" \
  -d "max_results=100" \
  -d "expansions=author_id" \
  -d "tweet.fields=created_at,public_metrics"
之后:Sorsa API
curl -X POST "https://api.sorsa.io/v3/search-tweets" \
  -H "ApiKey: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "from:mira_moves_reads", "order": "latest"}'
Sorsa API 接口
POST /search-tweets
一次请求返回
约 20 条推文
官方 X API,1,000 条结果
$5.00,作者另加最多 $10.00
Sorsa API,1,000 条结果
低至 $0.07

分页: next_token 变为 next_cursor (放在 JSON body 里)。同一条查询可回溯到 2006 年,而不是 7 天,并且每条推文都带有作者。

选择你的代码用的 v2 调用,查看 Sorsa API 上的同一请求,以及两边 1,000 条结果各要多少钱。

如果你的代码用了 filtered stream 或 webhook

Sorsa API 没有数据流,也没有 webhook,所以一条 filtered stream 规则要改成你轮询的搜索查询。把规则作为 POST /search-tweets 的查询发送,带上 "order": "latest",加上 since_id: 和你存下的最新 ID,再沿着 next_cursor 翻页,这样即使某一分钟新增超过 20 条帖子,也一条不漏。

poll.py
import os
import time
import requests

URL = "https://api.sorsa.io/v3/search-tweets"
HEADERS = {"ApiKey": os.environ.get("SORSA_API_KEY", "YOUR_API_KEY")}
RULE = '"open data" lang:en'


def new_tweets(since_id):
    body = {"query": f"{RULE} since_id:{since_id}", "order": "latest"}
    found = []
    while True:
        r = requests.post(URL, headers=HEADERS, json=body, timeout=30)
        if r.status_code == 429:
            time.sleep(1)
            continue
        r.raise_for_status()
        page = r.json()
        found += page.get("tweets", [])
        if not page.get("next_cursor"):
            return found
        body["next_cursor"] = page["next_cursor"]


newest = "1852331946812457001"
while True:
    tweets = new_tweets(newest)
    for t in sorted(tweets, key=lambda t: int(t["id"])):
        print(t["id"], t["user"]["username"], t["full_text"][:60])
    if tweets:
        newest = max((t["id"] for t in tweets), key=int)
    time.sleep(15)

所有套餐上,一个密钥都是每秒 20 次请求,也就是每分钟 1,200 次单页轮询,由你的各条规则共用。监控任务的调度、告警和去重,见推特实时监控指南。

#重写响应解析

Sorsa API 把每条推文作为一个对象发来,作者就在里面,所以 data 与 includes 之间的关联消失了,剩下的工作就是重命名字段。

官方 X API v2 Sorsa API GET /2/tweets/search/recent { "data": [{ "id": "18523319…", "text": "Read path moved…", "author_id": "15920418…", "public_metrics": { "like_count": 41 } }], "includes": {"users": [{ "id": "15920418…", "username": "mira_moves_reads" }]}, "meta": {"next_token": "b26v8…"} } 1 2 3 4 3 5 POST /search-tweets { "tweets": [{ "id": "18523319…", "full_text": "Read path moved…", "user": { "id": "15920418…", "username": "mira_moves_reads" }, "likes_count": 41 }], "next_cursor": "DAADDAAB…" } 1 2 3 4 5 作者放在每条推文里面: 无需请求字段或 expansions。
X API v2 把推文和作者分开,用 next_token 分页;Sorsa API 把作者放在推文里面,用 next_cursor 分页。

下面的字段名来自 X 的数据字典和 Sorsa API 的线上参考,读取于 2026 年 11 月 2 日。多数改动是拍平:某个指标离开 public_metrics,名字不变;少数是改名。

X API v2 字段Sorsa API 字段
data[]tweets[],账号列表则是 users[]
text 和 note_tweet.textfull_text
author_id 加 includes.users[]user,完整资料
public_metrics.like_countlikes_count
public_metrics.impression_countview_count
public_metrics.retweet_count、reply_count、quote_count、bookmark_count名字相同,上移一层
conversation_idconversation_id_str
referenced_tweets 加 includes.tweets[]quoted_status、retweeted_status、in_reply_to_tweet_id
attachments.media_keys 加 includes.media[]entities[],含 type、link、preview
用户的 namedisplay_name
用户的 public_metrics.following_count、tweet_countfollowings_count、tweets_count
meta.next_token 和 pagination_tokennext_cursor,两个方向都用它
含 title 和 detail 的 errors[]{"message": "..."}
  • data[]

    Sorsa API 字段
    tweets[],账号列表则是 users[]
  • text 和 note_tweet.text

    Sorsa API 字段
    full_text
  • author_id 加 includes.users[]

    Sorsa API 字段
    user,完整资料
  • public_metrics.like_count

    Sorsa API 字段
    likes_count
  • public_metrics.impression_count

    Sorsa API 字段
    view_count
  • public_metrics.retweet_count、reply_count、quote_count、bookmark_count

    Sorsa API 字段
    名字相同,上移一层
  • conversation_id

    Sorsa API 字段
    conversation_id_str
  • referenced_tweets 加 includes.tweets[]

    Sorsa API 字段
    quoted_status、retweeted_status、in_reply_to_tweet_id
  • attachments.media_keys 加 includes.media[]

    Sorsa API 字段
    entities[],含 type、link、preview
  • 用户的 name

    Sorsa API 字段
    display_name
  • 用户的 public_metrics.following_count、tweet_count

    Sorsa API 字段
    followings_count、tweets_count
  • meta.next_token 和 pagination_token

    Sorsa API 字段
    next_cursor,两个方向都用它
  • 含 title 和 detail 的 errors[]

    Sorsa API 字段
    {"message": "..."}

ID 仍是字符串,日期仍是 ISO 8601,所以已存的数据结构不变。分页统一成一个名字:在 GET 接口里把 next_cursor 作为查询参数发送,在 POST 接口里放进 JSON 请求体,返回为空时停止。限流也变简单了。我们的 X API 速率限制指南里描述的 15 分钟窗口和 x-rate-limit-* 请求头,换成了每个密钥每秒 20 次请求这一个限制:429 表示等一秒再重试,403 表示密钥缺失或本月请求额度已用完。

须知

每页的条数不是固定的。一页搜索结果最多有 20 条推文,在还有后续页的情况下也可能更少,所以要按 next_cursor 循环,永远不要按条目数量循环,否则一页偏短就会提前结束任务。

#Tweepy 和 xdk 代码,迁移前后

Tweepy 和 xdk 是免费的库;账单来自它们背后的官方 X API,所以迁移换的是客户端,不是语言。下面是同一个带作者、翻到底的搜索,先用 Tweepy,再用基于 Sorsa API 的普通 Python:

import tweepy

client = tweepy.Client(bearer_token="X_BEARER_TOKEN")

for page in tweepy.Paginator(
        client.search_recent_tweets,
        query="from:mira_moves_reads",
        tweet_fields=["created_at", "public_metrics"],
        expansions=["author_id"],
        user_fields=["username", "name"],
        max_results=100):
    users = {u.id: u for u in page.includes.get("users", [])}
    for t in page.data or []:
        author = users[t.author_id]
        print(t.id, author.username, t.public_metrics["like_count"])

字段列表、includes 关联和分页器从代码里消失了;多出来的只有一个请求头。xdk 代码的改法一样:Client(bearer_token=...) 和 client.posts.search_recent(...) 同样翻的是 data 和 includes,所以都换成上面的循环。迁移之后,查询的覆盖范围也更大了,因为 POST /search-tweets 在所有套餐上都能回溯搜索到 2006 年,而 v2 的近期搜索只覆盖最近 7 天。

基于 Node.js 的 twitter-api-v2 包的代码,迁移方式相同,换成 fetch 加一个请求头:

Node.js
const URL = "https://api.sorsa.io/v3/search-tweets";
const headers = {
  ApiKey: process.env.SORSA_API_KEY ?? "YOUR_API_KEY",
  "Content-Type": "application/json",
};

const body = { query: "from:mira_moves_reads", order: "latest" };
do {
  const res = await fetch(URL, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const page = await res.json();
  for (const t of page.tweets ?? []) {
    console.log(t.id, t.user.username, t.likes_count);
  }
  body.next_cursor = page.next_cursor;
} while (body.next_cursor);

对于只读取的 Tweepy 代码,把读取迁到 Sorsa API:搜索每次请求返回 20 条推文,每 1,000 条 $0.07 起;用户资料每次请求 100 份,每 1,000 份 $0.014 起;不需要 OAuth。重试、批量和每秒 20 次请求的限速器,见 Python 调用 Twitter API指南。

#转换你的搜索查询

POST /search-tweets 接受 X 高级搜索的语法,也就是 x.com 搜索框用的那套语言,所以关键词、带引号的短语、from:、to:、lang:、conversation_id: 和话题标签都原样沿用。要改的是 v2 的 is: 和 has: 操作符及其时间参数,它们要挪进查询里:

X API v2Sorsa API 查询
start_time 和 end_time 参数since:2026-10-01 和 until:2026-11-01
since_id 和 until_id 参数since_id:ID 和 max_id:ID(含该 ID)
sort_order=recency 或 relevancy请求体里的 "order": "latest" 或 "popular"
-is:retweet不用写:默认就不含转发
is:reply 和 -is:replyfilter:replies 和 -filter:replies
is:quotefilter:quote
has:media、has:images、has:linksfilter:media、filter:images、filter:links
has:video_linkfilter:native_video
min_likes:、min_reposts:、min_replies:min_faves:、min_retweets:、min_replies:
point_radius:geocode:
  • start_time 和 end_time 参数

    Sorsa API 查询
    since:2026-10-01 和 until:2026-11-01
  • since_id 和 until_id 参数

    Sorsa API 查询
    since_id:ID 和 max_id:ID(含该 ID)
  • sort_order=recency 或 relevancy

    Sorsa API 查询
    请求体里的 "order": "latest" 或 "popular"
  • -is:retweet

    Sorsa API 查询
    不用写:默认就不含转发
  • is:reply 和 -is:reply

    Sorsa API 查询
    filter:replies 和 -filter:replies
  • is:quote

    Sorsa API 查询
    filter:quote
  • has:media、has:images、has:links

    Sorsa API 查询
    filter:media、filter:images、filter:links
  • has:video_link

    Sorsa API 查询
    filter:native_video
  • min_likes:、min_reposts:、min_replies:

    Sorsa API 查询
    min_faves:、min_retweets:、min_replies:
  • point_radius:

    Sorsa API 查询
    geocode:

有三个细节容易让转换后的查询出错。原生转发默认被排除,除非加上 include:nativeretweets。geocode: 先写纬度,如 geocode:37.77,-122.41,5km,而 v2 的 point_radius: 先写经度。另外 max_id: 包含这个 ID 本身,而 v2 的 until_id 不含。完整的操作符列表和用法示例,见我们的 Twitter 搜索操作符指南。

#用影子运行测试,然后切换

影子运行,是在官方 X API 继续为你的应用服务的同时,把每个读取复制一份发给 Sorsa API,这样在任何东西依赖它之前,先把结果对比一遍。

阶段 1 影子 X API 负责所有 读取;Sorsa API 收到一份副本 读取 X API 100% Sorsa API 副本 阶段 2 对比 逐个查询对比 推文 ID、字段 和错误 读取 X API 100% Sorsa API 副本 阶段 3 金丝雀 10% 的读取来自 Sorsa API; 盯着错误 读取 X API 90% Sorsa API 10% 阶段 4 切换 所有读取都走 Sorsa API;删掉 旧的读取代码 读取 Sorsa API 100% 写入 每个阶段都用官方 X API:发帖、回复、私信
读取分四个阶段迁移;写入始终不离开官方 X API。

这个脚本在两套 API 上对同一个已结束的日期,运行同一个查询,并比较帖子 ID。对你依赖的每个查询和每条时间线都运行一遍;用已结束的时间窗口,可以避免新帖子扭曲计数。

shadow_run.py
import os
import requests

X_URL = "https://api.x.com/2/tweets/search/recent"
X_HEADERS = {"Authorization": "Bearer " + os.environ["X_BEARER_TOKEN"]}
S_URL = "https://api.sorsa.io/v3/search-tweets"
S_HEADERS = {"ApiKey": os.environ.get("SORSA_API_KEY", "YOUR_API_KEY")}


def x_ids(query, start, end):
    params = {"query": query, "start_time": start,
              "end_time": end, "max_results": 100}
    ids = set()
    while True:
        r = requests.get(X_URL, headers=X_HEADERS, params=params, timeout=30)
        r.raise_for_status()
        page = r.json()
        ids |= {t["id"] for t in page.get("data", [])}
        token = page.get("meta", {}).get("next_token")
        if not token:
            return ids
        params["next_token"] = token


def sorsa_ids(query, since, until):
    body = {"query": f"{query} since:{since} until:{until}",
            "order": "latest"}
    ids = set()
    while True:
        r = requests.post(S_URL, headers=S_HEADERS, json=body, timeout=30)
        r.raise_for_status()
        page = r.json()
        ids |= {t["id"] for t in page.get("tweets", [])}
        if not page.get("next_cursor"):
            return ids
        body["next_cursor"] = page["next_cursor"]


query = "from:mira_moves_reads"
a = x_ids(query, "2026-10-30T00:00:00Z", "2026-10-31T00:00:00Z")
b = sorsa_ids(query, "2026-10-30_00:00:00_UTC", "2026-10-31_00:00:00_UTC")
print(f"official X API {len(a)}, Sorsa API {len(b)}, shared {len(a & b)}")
print("only on X:", sorted(a - b)[:10])
print("only on Sorsa API:", sorted(b - a)[:10])

预期会有小的差异,而不是零差异:两次调用之间被删除的帖子会消失,而且 X 的关键词搜索自 2026 年 5 月 4 日起就不含转发。X 的近期搜索只能回溯 7 天,所以时间窗口要放在最近一周以内。等你最常用的那些读取,ID 都对得上了,就分阶段切换:

  • 把两个客户端放到同一个接口后面,并用一个配置开关控制,这样任何一类读取都可以只改一个值就迁回去。
  • 先迁 10% 的读取,观察错误和 GET /key-usage-info(它免费,从不计入你的请求数),然后再迁其余部分。
  • 官方 X API 的密钥只留给写入,并在开发者控制台里调低它的消费上限和自动充值金额。
  • 删除读取的代码路径、字段列表和旧的分页逻辑,等一个完整的计费周期过去之后再做。

#把你的读取迁出官方 X API

只读的 v2 集成迁到 Sorsa API,这个专为读取 X 公开数据而生的 Twitter API,只需一个请求头,每个调用对应一个接口,解析器也不再需要关联。搜索每 1,000 条推文 $0.07 起,而不是 $5.00;粉丝每 1,000 个 $0.007 起,而不是 $10.00;所有套餐都是每秒 20 次请求。从你最贵的那个读取开始,影子运行几天,官方 X API 只留给写入。

#常见问题

从官方 X API 迁移,该迁到哪个 Twitter API?

要读取 X 公开数据,迁到 Sorsa API:每个 v2 读取都对应一个接口,按 ID 查询推文每 1,000 条 $0.014 起,搜索 $0.07 起,而官方 X API 是 $5.00,所有套餐都是每秒 20 次请求,搜索可回溯到 2006 年。一个 API 密钥就能取代 OAuth。Sorsa API 是只读的,所以发帖仍请保留官方 X API。

从官方 X API 迁出要多久?

代码改动量取决于有多少种不同的 v2 调用,而不是你的数据量:每个调用变成一个接口、一个请求头和几个改名的字段。再加上几天的影子运行,期间两套 API 回答同样的读取,你对比 ID。发帖、私信和其他写入留在官方 X API,所以只有读取路径要迁。

把读取迁到 Sorsa API 之后,还能继续往 X 发帖吗?

能。发帖、回复和私信继续用官方 X API,读取则发给 Sorsa API。Sorsa API 在设计上是只读的:它的密钥无法操作任何 X 账号,也从不需要你的 X 登录信息,所以两者并存互不冲突。如果你的写入路径仍用 OAuth 1.0a 用户令牌签名,X 自 2026 年 9 月 21 日起已允许应用把它们换成 OAuth 2.0 令牌。

我需要先从 v1.1 迁到 v2 吗?

不需要。仍在调用 v1.1 读取的代码,可以直接映射到 Sorsa API:statuses/user_timeline 变成 POST /user-tweets,statuses/show 变成 POST /tweet-info,users/show 变成 GET /info。X 把 v1.1 列为旧版、只提供有限支持,并在 2023 年弃用了 v1.1 的搜索,所以停在 v2 意味着你得先为 OAuth 2.0 和按量付费计费重写一遍,之后还得再迁一次。

旧版 Basic 和 Pro 的 X API 套餐怎么样了?

X 把它们迁到了按量付费:旧版 Basic 订阅在 2026 年 6 月 1 日之后的切换日期终止,Pro 在 2026 年 9 月 1 日之后终止。Pro 月付账号在切换时被充入 $5,000 的额度,并开启自动充值。现在读取按资源计费,每条帖子 $0.005,每个用户 $0.010,而同样的推文在 Sorsa API 上通过搜索每 1,000 条 $0.07 起。

#我们如何核实

2026 年 11 月 2 日,我们把 X 自己的 OpenAPI 规范(版本 2.173,200 个路径)与 Sorsa API 的线上参考(45 个接口)逐一比对,做出接口对照表,并对照 X 的操作符列表核对了每个 v2 操作符,对照 Sorsa API 的搜索操作符核对了每个替代项。同一天,我们阅读了 X 的更新日志、价格页面、分页和速率限制文档、Python XDK 参考和 Tweepy 4.17.0 的客户端,以及 X 关于搜索索引、旧版套餐和 expansions 计费的论坛通知。这一轮我们没有 Sorsa API 密钥,所以展示的响应使用的是演示值;Python 和 Node.js 代码块,我们是对着一个本地模拟服务运行的,它按两套 API 文档里的结构作答。

Sorsa API 团队撰写。 我们开发和运营 Sorsa API,这是只读的 Twitter/X API 与爬虫,2022 年起运行于生产环境(前身为 TweetScout API),累计处理请求超 60 亿次。 关于 Sorsa API

更新内容
  • 作为迁移指南全文重写:X 在 2023 到 2026 年的变化及每项变化会破坏什么,依据两边规范整理的 v2 到 Sorsa API 接口对照表,请求转换器,字段与操作符对照,Tweepy 迁移前后的代码,用轮询代替数据流,以及影子运行测试计划。
  • 首次发布。