Twitter/X API から移行する方法
公式 X API からの移行で必要なコード変更は5つです。v2 の各エンドポイントを新しいパスに対応させ、OAuth を API キーに置き換え、フィールドと expansions を外し、JSON のパースを書き直し、ページネーショントークンを切り替えます。
結論: 移行すべきなのは読み取りです。公式 X API は1投稿あたり$0.005、1ユーザーあたり$0.010を課金し、2026年11月22日からは投稿に付いて返る投稿者1人ごとに$0.010が加算されます。読み取りの移行先には Sorsa API を使います。ApiKey ヘッダー1つで、検索は1リクエスト20ツイート、1,000件あたり$0.07からです。対応表の検証には、100回分の無料リクエストを使えます。
- エンドポイント
POST /search-tweets- 1ページの件数
- 1リクエストあたり20ツイート
- 料金
- 1,000件あたり$0.07から
#2023年以降、公式 X API で何が変わったか
公式 X API 向けに書かれたコードは、ほとんどそのまま動きます。2023年以降に変わったのは、呼び出せるもの、返ってくるもの、そしてレスポンスごとの費用です。以下の日付入りの一覧は、X の変更履歴(新しいタブで開きます)と開発者フォーラムのお知らせに基づき、各変更が連携のどこに影響するかを添えたものです。
| 日付 | 公式 X API の変更 | コードへの影響 |
|---|---|---|
| 2023年3月30日 | 無料アクセスが有料プランに置き換わり、Free は書き込み専用に | 無料アプリでの読み取りがエラーになる |
| 2023年6月26日 | Basic と Pro からフォロー系エンドポイントが削除 | そのプランでフォロワーとフォロー中の呼び出しが止まる |
| 2023年9月28日 | v1.1 の search/tweets と users/search が非推奨に | v1.1 の検索コードは新しいエンドポイントが必要 |
| 2026年1月19日 | 検索から min_likes、min_replies、min_reposts が削除 | これらを使ったクエリは5月4日まで外す必要があった |
| 2026年2月6日 | 従量課金がモデルになり、クレジットは前払い | 残高がゼロになると呼び出しがブロックされる |
| 2026年2月23日 | プログラムによる返信は、投稿者から呼びかけられた場合のみ許可 | 返信ボットが拒否される |
| 2026年4月20日 | セルフサービスからフォロー、いいね、引用ポストが削除 | これらの書き込み呼び出しが動かなくなる |
| 2026年5月4日 | 検索が新しいインデックスに移行し、キーワード検索結果からリツイートが除外 | 同じクエリで返る投稿が減る |
| 2026年6月1日と9月1日 | 旧 Basic、続いて旧 Pro が従量課金へ移行 | 月間の上限が、リソースごとの請求に変わる |
| 2026年11月22日 | expansions が課金対象に:投稿者1人$0.010、メディア1件$0.005 | expansions=author_id で投稿1件の費用が3倍になり得る |
2023年3月30日
- 公式 X API の変更
- 無料アクセスが有料プランに置き換わり、Free は書き込み専用に
- コードへの影響
- 無料アプリでの読み取りがエラーになる
2023年6月26日
- 公式 X API の変更
- Basic と Pro からフォロー系エンドポイントが削除
- コードへの影響
- そのプランでフォロワーとフォロー中の呼び出しが止まる
2023年9月28日
- 公式 X API の変更
- v1.1 の
search/tweetsとusers/searchが非推奨に - コードへの影響
- v1.1 の検索コードは新しいエンドポイントが必要
2026年1月19日
- 公式 X API の変更
- 検索から
min_likes、min_replies、min_repostsが削除 - コードへの影響
- これらを使ったクエリは5月4日まで外す必要があった
2026年2月6日
- 公式 X API の変更
- 従量課金がモデルになり、クレジットは前払い
- コードへの影響
- 残高がゼロになると呼び出しがブロックされる
2026年2月23日
- 公式 X API の変更
- プログラムによる返信は、投稿者から呼びかけられた場合のみ許可
- コードへの影響
- 返信ボットが拒否される
2026年4月20日
- 公式 X API の変更
- セルフサービスからフォロー、いいね、引用ポストが削除
- コードへの影響
- これらの書き込み呼び出しが動かなくなる
2026年5月4日
- 公式 X API の変更
- 検索が新しいインデックスに移行し、キーワード検索結果からリツイートが除外
- コードへの影響
- 同じクエリで返る投稿が減る
2026年6月1日と9月1日
- 公式 X API の変更
- 旧 Basic、続いて旧 Pro が従量課金へ移行
- コードへの影響
- 月間の上限が、リソースごとの請求に変わる
2026年11月22日
- 公式 X API の変更
- expansions が課金対象に:投稿者1人$0.010、メディア1件$0.005
- コードへの影響
expansions=author_idで投稿1件の費用が3倍になり得る
このうち2つは、コードを1行も変えていないのに結果が変わりました。5月4日に新しい検索インデックス(新しいタブで開きます)が稼働して以降、4月には問題なく動いたキーワード検索が、リツイートなしで返ってきます。そして11月22日からは、1,000投稿あたり$5.00だった expansions=author_id 付きの同じリクエストが最大$15.00になります。X のexpansions のお知らせ(新しいタブで開きます)が、返される投稿者をユーザーの読み取りとして課金するためです。
稼働中のコードの足元でプランも変わりました。X は旧 Basic(新しいタブで開きます) のサブスクリプションを2026年6月1日以降の切り替え日に終了し、旧 Pro(新しいタブで開きます) は9月1日以降に終了しました。月払いの Pro アカウントには、切り替え時に$5,000のクレジットが請求され、自動チャージがオンになりました。現在、読み取りはすべて、1投稿あたり$0.005、1ユーザーまたはフォロワーあたり$0.010のクレジットから差し引かれ、月300万投稿の読み取りが上限です。全料金表はX API の料金の解説にまとめています。
#Sorsa API に移るものと X に残るもの
公開データの読み取りはすべて移行できます。検索、タイムライン、投稿とユーザーの取得、フォロワー、リスト、引用、リツイートしたユーザー、リプライ、スペース、コミュニティ、トレンドです。2022年から本番稼働し、累計60億リクエスト以上を処理してきた Twitter(X)API 兼スクレイピングツールである Sorsa API は、これらをすべてキー1つで返します。OAuth は不要で、開発者アカウントなしで使う Twitter APIのガイドで説明しているとおり、X の開発者アカウントも要りません。設計上、読み取り専用なので、書き込みの呼び出しは今の場所に残ります。
次のものは、公式 X API に残します。
- 投稿、返信、削除、メディアのアップロード。
- ダイレクトメッセージ、およびいいね、フォロー、ブックマーク、ブロック(操作としても、自分のアカウントの活動一覧としても)。
- 投稿にいいねしたユーザー、および非公開アカウントのあらゆるデータ。
- フィルタードストリーム、ファイアホース、Webhook、X Activity API:Sorsa API はプッシュではなくポーリングで使います(ポーリングのパターンは下で紹介します)。
- 投稿数のカウント、コミュニティノート、Ads API。
読み取りに X アカウントは関与しません。Sorsa API が、ログイン情報や Cookie を求めることはないからです。移行後に公式 X API が課金するのは、書き込みだけです。リクエストごとに、投稿の作成が$0.015、リンク付きの投稿が$0.20です。
お金がかかるのは読み取りです。検索とタイムラインの読み取りで、100万ツイートは公式 X API なら投稿者への課金を除いても$5,000かかり、Sorsa API なら$70からです。Sorsa API は各ツイートの投稿者を追加料金なしで一緒に返します。ID での取得はさらに安く、POST /tweet-info-bulk は1リクエストで100ツイートを返し、1,000件あたり$0.014からです。
公式 X API
$5,000
1Mツイートの場合
1投稿あたり$0.005、投稿者を除く
Sorsa API
$70から
1Mツイートの場合
1,000件あたり$0.07から、投稿者込み
#6ステップで Sorsa API へ移行する
v2 の連携を棚卸しから切り替えまで進めるのに、6つのステップがあります。最初の5つはコードの修正で、最後の1つは両方の API が同じ読み取りに答える状態で数日間続けます。
-
コードが行うすべての呼び出しを洗い出す。 コード内で
api.x.com/2、api.twitter.com/2、使っているクライアントのメソッド名(Tweepy のsearch_recent_tweetsや xdk のposts.search_recentなど)を検索します。X の Developer Console から先月の利用状況をリソース別にエクスポートし、各呼び出しを読み取りか書き込みかに分類してください。 -
キーを取得して1リクエスト送る。 サインアップして、ダッシュボードからキーをコピーし、いちばん多く使っている読み取りに代わるエンドポイントを呼び出します。
ターミナル 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" } -
各読み取りをエンドポイントに対応させる。 次のセクションの表と変換ツールが、v2 の読み取りごとに Sorsa API のパスとパラメータを対応させています。
-
OAuth、フィールド、expansions を1つのヘッダーに置き換える。 読み取りの呼び出しでは、Bearer トークンや OAuth 1.0a の署名を削除し、代わりに
ApiKey: YOUR_API_KEYを送ります。tweet.fields、user.fields、expansionsは外してください。すべてのフィールドと投稿者が、デフォルトで返ってきます。 -
パースとページネーションを書き直す。
dataとincludesを読んでいた箇所でtweetsまたはusersを読み、名前が違うフィールドを置き換え、next_cursorが空で返るまで送り返します。 -
シャドウランを実施してから切り替える。 読み取りのコピーを数日間 Sorsa API に送って結果を比較し、トラフィックを段階的に移し、X のキーは書き込み用に残します。
#X API v2 の各エンドポイントを Sorsa API に対応させる
連携が呼び出しそうな公開データの読み取りには、それぞれ対応する Sorsa API のエンドポイントがあります。下の表は、X の OpenAPI 仕様(新しいタブで開きます)に書かれた v2 のパスごとに、置き換え先と、1リクエストで返る量を対応させたものです。Sorsa API は件数ではなくリクエスト単位で課金するため、最後の列が料金を決めます。
| X API v2 の呼び出し | Sorsa API のエンドポイント | 1リクエストで返る量 |
|---|---|---|
GET /2/tweets/search/recent | POST /search-tweets(新しいタブで開きます) | 約20ツイート |
GET /2/tweets/search/all | since: と until: を付けた POST /search-tweets | 約20ツイート |
GET /2/tweets/:id | POST /tweet-info(新しいタブで開きます) | 1ツイート |
GET /2/tweets(ID 指定) | POST /tweet-info-bulk(新しいタブで開きます) | 最大100ツイート |
GET /2/users/:id/tweets | POST /user-tweets(新しいタブで開きます) | 約20ツイート |
GET /2/users/:id/mentions | POST /mentions(新しいタブで開きます) | 約20ツイート |
GET /2/tweets/:id/quote_tweets | POST /quotes(新しいタブで開きます) | 約20ツイート |
GET /2/tweets/:id/retweeted_by | POST /retweeters(新しいタブで開きます) | プロフィール1ページ分 |
conversation_id: を使った検索 | POST /comments(新しいタブで開きます) | 約20件のリプライ |
GET /2/users/by/username/:username | GET /info(新しいタブで開きます) | 1プロフィール |
GET /2/users と GET /2/users/by | GET /info-batch(新しいタブで開きます) | 最大100プロフィール |
GET /2/users/:id/followers | GET /followers(新しいタブで開きます) | 最大200プロフィール |
GET /2/users/:id/following | GET /follows(新しいタブで開きます) | 最大200プロフィール |
GET /2/users/search | POST /search-users(新しいタブで開きます) | プロフィール1ページ分 |
GET /2/lists/:id/members | GET /list-members(新しいタブで開きます) | プロフィール1ページ分 |
GET /2/lists/:id/followers | GET /list-followers(新しいタブで開きます) | プロフィール1ページ分 |
GET /2/lists/:id/tweets | GET /list-tweets(新しいタブで開きます) | 約20ツイート |
GET /2/spaces/:id | GET /spaces(新しいタブで開きます) | 1スペース |
GET /2/communities/:id | POST /community-info(新しいタブで開きます) | 1コミュニティ |
GET /2/communities/search | POST /search-communities(新しいタブで開きます) | コミュニティ1ページ分 |
GET /2/trends/by/woeid/:woeid | GET /trends(新しいタブで開きます) | 1地域のトレンド |
GET /2/usage/tweets | GET /key-usage-info(新しいタブで開きます) | 残高(無料) |
GET /2/tweets/search/recent- Sorsa API のエンドポイント
POST /search-tweets(新しいタブで開きます)- 1リクエストで返る量
- 約20ツイート
GET /2/tweets/search/all- Sorsa API のエンドポイント
since:とuntil:を付けたPOST /search-tweets- 1リクエストで返る量
- 約20ツイート
GET /2/tweets/:id- Sorsa API のエンドポイント
POST /tweet-info(新しいタブで開きます)- 1リクエストで返る量
- 1ツイート
GET /2/tweets(ID 指定)- Sorsa API のエンドポイント
POST /tweet-info-bulk(新しいタブで開きます)- 1リクエストで返る量
- 最大100ツイート
GET /2/users/:id/tweets- Sorsa API のエンドポイント
POST /user-tweets(新しいタブで開きます)- 1リクエストで返る量
- 約20ツイート
GET /2/users/:id/mentions- Sorsa API のエンドポイント
POST /mentions(新しいタブで開きます)- 1リクエストで返る量
- 約20ツイート
GET /2/tweets/:id/quote_tweets- Sorsa API のエンドポイント
POST /quotes(新しいタブで開きます)- 1リクエストで返る量
- 約20ツイート
GET /2/tweets/:id/retweeted_by- Sorsa API のエンドポイント
POST /retweeters(新しいタブで開きます)- 1リクエストで返る量
- プロフィール1ページ分
conversation_id:を使った検索- Sorsa API のエンドポイント
POST /comments(新しいタブで開きます)- 1リクエストで返る量
- 約20件のリプライ
GET /2/users/by/username/:username- Sorsa API のエンドポイント
GET /info(新しいタブで開きます)- 1リクエストで返る量
- 1プロフィール
GET /2/usersとGET /2/users/by- Sorsa API のエンドポイント
GET /info-batch(新しいタブで開きます)- 1リクエストで返る量
- 最大100プロフィール
GET /2/users/:id/followers- Sorsa API のエンドポイント
GET /followers(新しいタブで開きます)- 1リクエストで返る量
- 最大200プロフィール
GET /2/users/:id/following- Sorsa API のエンドポイント
GET /follows(新しいタブで開きます)- 1リクエストで返る量
- 最大200プロフィール
GET /2/users/search- Sorsa API のエンドポイント
POST /search-users(新しいタブで開きます)- 1リクエストで返る量
- プロフィール1ページ分
GET /2/lists/:id/members- Sorsa API のエンドポイント
GET /list-members(新しいタブで開きます)- 1リクエストで返る量
- プロフィール1ページ分
GET /2/lists/:id/followers- Sorsa API のエンドポイント
GET /list-followers(新しいタブで開きます)- 1リクエストで返る量
- プロフィール1ページ分
GET /2/lists/:id/tweets- Sorsa API のエンドポイント
GET /list-tweets(新しいタブで開きます)- 1リクエストで返る量
- 約20ツイート
GET /2/spaces/:id- Sorsa API のエンドポイント
GET /spaces(新しいタブで開きます)- 1リクエストで返る量
- 1スペース
GET /2/communities/:id- Sorsa API のエンドポイント
POST /community-info(新しいタブで開きます)- 1リクエストで返る量
- 1コミュニティ
GET /2/communities/search- Sorsa API のエンドポイント
POST /search-communities(新しいタブで開きます)- 1リクエストで返る量
- コミュニティ1ページ分
GET /2/trends/by/woeid/:woeid- Sorsa API のエンドポイント
GET /trends(新しいタブで開きます)- 1リクエストで返る量
- 1地域のトレンド
GET /2/usage/tweets- Sorsa API のエンドポイント
GET /key-usage-info(新しいタブで開きます)- 1リクエストで返る量
- 残高(無料)
パスの変更に伴い、4つの習慣が変わります。ツイート系のエンドポイントは JSON ボディを持つ POST で、ユーザー、リスト、ユーティリティの取得は GET のままです。v2 が数値 ID を求めていた場所でも、ユーザー名が使えるため、タイムライン呼び出しの前に必要だった追加の取得は不要になります。tweet_link には、投稿の完全な URL でも ID だけでも渡せます。そして POST /user-tweets は、X のユーザータイムラインが止まる最新3,200件を超えてもページをたどれます。
v2 の読み取りの一部には対応するものがありません。Sorsa API が返すのは、公開プロフィールで誰でも見られる情報だけだからです。投稿にいいねしたユーザー、ユーザーのいいねとブックマーク、投稿数のカウント、ホームタイムライン、すべてのストリームがそれにあたります。逆に、v2 では答えられないことにも答えます。POST /check-follow(新しいタブで開きます) は、あるアカウントが別のアカウントをフォローしているかを1リクエストで返し、GET /about(新しいタブで開きます) はアカウントの国とユーザー名の履歴を返します。
下の変換ツールは、v2 の呼び出しを1つずつ Sorsa API のリクエストに変換し、変わるフィールドと、双方の1,000件あたりの料金を示します。
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"
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- 1回のリクエストで返る数
- 約20ツイート
- 公式X API、1,000件の結果
- $5.00、さらに投稿者分で最大$10.00
- Sorsa API、1,000件の結果
- 最安 $0.07
ページネーション: next_token は次に置き換わります: next_cursor (JSONボディで送ります)。同じクエリで、7日ではなく2006年までさかのぼって検索でき、すべてのツイートに投稿者情報が付きます。
コードで使っているv2の呼び出しを選ぶと、Sorsa APIでの同じリクエストと、それぞれでの1,000件の費用が表示されます。
フィルタードストリームや Webhook を使っているコードの場合
Sorsa API にはストリームも Webhook もないため、フィルタードストリームのルールは、ポーリングする検索クエリに置き換わります。ルールを POST /search-tweets のクエリとして "order": "latest" 付きで送り、保存済みの最新 ID を since_id: に指定し、next_cursor をたどります。こうすれば、新規投稿が20件を超える忙しい1分間でも、取りこぼしはありません。
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)
1つのキーは、どのプランでも毎秒20リクエストで動作します。これは、ルール全体で共有する1ページ分のポーリングなら1分間に1,200回になります。監視ジョブのスケジュール、アラート、重複排除については、リアルタイム Twitter 監視のガイドで扱っています。
#レスポンスのパースを書き直す
Sorsa API は、各ツイートを投稿者込みの1つのオブジェクトとして返します。そのため data と includes の結合は不要になり、残る作業はフィールド名の置き換えです。
以下のフィールド名は、2026年11月2日に確認した X のデータディクショナリと Sorsa API の公開リファレンスに基づいています。多くの変更はフラット化で、メトリクスが public_metrics から出て同じ名前のまま残ります。それ以外は一部の名前の変更です。
| X API v2 のフィールド | Sorsa API のフィールド |
|---|---|
data[] | tweets[]。アカウントの一覧は users[] |
text と note_tweet.text | full_text |
author_id と includes.users[] | user(プロフィール全体) |
public_metrics.like_count | likes_count |
public_metrics.impression_count | view_count |
public_metrics.retweet_count、reply_count、quote_count、bookmark_count | 同じ名前で、1階層上 |
conversation_id | conversation_id_str |
referenced_tweets と includes.tweets[] | quoted_status、retweeted_status、in_reply_to_tweet_id |
attachments.media_keys と includes.media[] | type、link、preview を持つ entities[] |
ユーザーの name | display_name |
ユーザーの public_metrics.following_count、tweet_count | followings_count、tweets_count |
meta.next_token と pagination_token | next_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 のフィールド
- 同じ名前で、1階層上
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 のフィールド
type、link、previewを持つentities[]
ユーザーの
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 のままなので、保存済みのデータの形は変わりません。ページネーションは1つの名前にまとまります。GET のエンドポイントではクエリパラメータとして、POST のエンドポイントでは JSON ボディの中に next_cursor を入れて送り、空で返ってきたら止めます。レート制限もシンプルになります。X API のレート制限のガイドで説明している15分ウィンドウと x-rate-limit-* ヘッダーは、キーごとに毎秒20リクエストという1つの制限に置き換わります。429 なら1秒待って再試行し、403 ならキーがないか、その月のリクエストを使い切っています。
知っておきたいこと
ページサイズは固定ではありません。検索の1ページには最大20ツイートが入りますが、後続のページが残っていても、それより少ないことがあります。ループは項目数ではなく必ず
next_cursorで回してください。そうしないと、短いページ1つで処理が早く終わってしまいます。
#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 の結合、ページネーターはコードから消え、ヘッダーが1つ入ります。xdk のコードも同じように変わります。Client(bearer_token=...) と client.posts.search_recent(...) は同じ data と includes をたどるので、上のループに置き換わります。移行後はクエリが届く範囲も広がります。POST /search-tweets はどのプランでも2006年まで遡って検索しますが、v2 の最近の投稿の検索は直近7日間だけです。
Node.js の twitter-api-v2 パッケージで作ったコードも同じ方法で、fetch とヘッダー1つに移れます。
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 に移します。検索は1リクエスト20ツイートで1,000件あたり$0.07から、プロフィールは1リクエスト100件で1,000件あたり$0.014から、OAuth は不要です。リトライ、バッチ処理、毎秒20リクエスト用のペース制御については、Python で使う Twitter APIのガイドをご覧ください。
#検索クエリを変換する
POST /search-tweets は、x.com の検索ボックスの言語である X の高度な検索の構文を受け付けます。そのため、キーワード、引用符で囲んだフレーズ、from:、to:、lang:、conversation_id:、ハッシュタグはそのまま使えます。変わるのは、v2 の is: と has: の演算子と時間のパラメータで、これらはクエリの中に移ります。
| X API v2 | Sorsa 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:reply | filter:replies と -filter:replies |
is:quote | filter:quote |
has:media、has:images、has:links | filter:media、filter:images、filter:links |
has:video_link | filter: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:
変換したクエリでつまずきやすい点が3つあります。ネイティブのリツイートは、include:nativeretweets を付けない限り除外されます。geocode: は geocode:37.77,-122.41,5km のように緯度が先で、v2 の point_radius: は経度が先でした。そして max_id: は ID 自体を含みますが、v2 の until_id は含みません。演算子の完全な一覧とレシピは、Twitter 検索演算子のガイドにあります。
#シャドウランでテストしてから切り替える
シャドウランとは、公式 X API がアプリへの応答を続けている間に、各読み取りのコピーを Sorsa API に送る方法です。何かが依存する前に、結果を比較できます。
このスクリプトは、両方の API で同じ終了済みの1日分に1つのクエリを実行し、投稿 ID を比較します。依存しているクエリとタイムラインごとに実行してください。期間を終了済みにしておくと、新しい投稿で件数がずれることを防げます。
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])
差がゼロになるとは限らず、小さな差が出るのが普通です。2回の呼び出しの間に削除された投稿は消えますし、X のキーワード検索は2026年5月4日からリツイートを除外しています。X の最近の投稿の検索は7日前までなので、期間は直近1週間以内にしてください。最もよく実行する読み取りで ID が揃ったら、段階的に切り替えます。
- 2つのクライアントを1つのインターフェースと1つの設定フラグの裏に置く。 どの種類の読み取りも、値を1つ変えるだけで元に戻せます。
- 読み取りの10%を移し、 エラーと
GET /key-usage-info(無料で、リクエストにカウントされません)を監視してから、残りを移します。 - 公式 X API のキーは書き込み専用にして、 Developer Console で利用上限と自動チャージの金額を下げます。
- 読み取りのコードパス、 フィールドのリスト、古いページネーションは、請求サイクルが1周してから削除します。
#公式 X API から読み取りを移す
読み取り専用の v2 連携は、X の公開データを読むために作られた Twitter API である Sorsa API に、ヘッダー1つ、呼び出しごとに1エンドポイント、結合が不要になったパーサーで移行できます。検索は1,000ツイートあたり$5.00ではなく$0.07から、フォロワーは1,000件あたり$10.00ではなく$0.007からで、どのプランも毎秒20リクエストで動作します。まず最も費用のかかる読み取りから始めて数日間シャドウランを行い、公式 X API は書き込みにだけ残してください。
#FAQ
公式 X API から、どの Twitter API に移行すべきですか
X の公開データの読み取りなら、Sorsa API に移行してください。v2 の各読み取りが1つのエンドポイントに対応し、ツイートは ID 指定で1,000件あたり$0.014から、検索で$0.07からです。公式 X API は$5.00です。どのプランも毎秒20リクエストで動作し、検索は2006年まで遡れます。OAuth の代わりに API キー1つで済みます。Sorsa API は読み取り専用なので、投稿には公式 X API を残してください。
公式 X API からの移行にはどのくらいかかりますか
コードの変更量は、データ量ではなく、v2 の呼び出しの種類の数に比例します。各呼び出しが、1つのエンドポイント、1つのヘッダー、いくつかのフィールド名の変更になります。そこに、両方の API が同じ読み取りに答え、ID を比較する数日間のシャドウランが加わります。投稿、DM、その他の書き込みは公式 X API に残るため、移るのは読み取り処理だけです。
読み取りを Sorsa API に移しても、X への投稿は続けられますか
はい。投稿、返信、DM には公式 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のクレジットが請求され、自動チャージがオンになりました。読み取りは現在リソースごとの課金で、1投稿$0.005、1ユーザー$0.010です。同じツイートが、Sorsa API では検索で1,000件あたり$0.07からです。
#検証の方法
2026年11月2日に、X 自身の OpenAPI 仕様(バージョン2.173、200パス)と Sorsa API の公開リファレンス(45エンドポイント)を突き合わせてエンドポイントの対応表を作成し、v2 の演算子をすべて X の演算子一覧と、置き換え先をすべて Sorsa API の検索演算子と照合しました。同じ日に、X の変更履歴、料金ページ、ページネーションとレート制限のドキュメント、Python XDK のリファレンス、Tweepy 4.17.0 のクライアント、さらに検索インデックス、旧プラン、expansions 課金に関する X のフォーラムのお知らせを読みました。今回の確認では Sorsa API のキーがなかったため、掲載したレスポンスはデモの値です。Python と Node.js のコードブロックは、両 API のドキュメントどおりの形で応答するローカルのモックに対して実行しました。
情報源
- X API changelog (docs.x.com)(新しいタブで開きます)
- X API pay-per-usage pricing (docs.x.com)(新しいタブで開きます)
- X API OpenAPI specification(新しいタブで開きます)
- Search operators, X API v2 (docs.x.com)(新しいタブで開きます)
- Search endpoints migrating to a new index (X Developers, May 2026)(新しいタブで開きます)
- Legacy Basic plans moving to pay-per-use (X Developers, May 21, 2026)(新しいタブで開きます)
- Legacy Pro plans moving to pay-per-use (X Developers, August 14, 2026)(新しいタブで開きます)
- X API PPU: expansions update (X Developers, October 23, 2026)(新しいタブで開きます)
- Python XDK (docs.x.com)(新しいタブで開きます)
- Sorsa API reference, OpenAPI
- Migration from the official X API (Sorsa API docs)(新しいタブで開きます)
- Search operators (Sorsa API docs)(新しいタブで開きます)