本文へスキップ

PHP での Twitter API:cURL、Guzzle、Laravel で X データを取得

PHP で Twitter/X データを取得するには、cURL か Guzzle で HTTPS リクエストを送り、ヘッダーに API キーを付け、JSON をデコードして、カーソルをたどりながら1ページずつ読みます。公式 X API は投稿1件につき$0.005かかります。

結論: X が公式 SDK を出しているのは Python と TypeScript だけです。そのため PHP から公式 X API を使うには、TwitterOAuth のようなコミュニティ製クライアントに加え、開発者アカウントと前払いクレジットが必要です。料金は投稿1件につき$0.005、2026年11月22日からは投稿者を含めると$0.015です。公開データの取得なら Sorsa API を使います。キー1つ、1リクエスト20ツイート、1,000件あたり$0.07から。100回分の無料リクエストで2,000ツイートをカバーできます。

エンドポイント
POST /user-tweets
ページサイズ
1リクエストあたり20ツイート
料金
1,000ツイートあたり$0.07から

#PHP 向けの公式 X API SDK はありますか?

ありません。X が公式 SDK を公開しているのは Python と TypeScript だけです。ツールのページには、X がメンテナンスしていないコミュニティ製の PHP クライアントが3つ載っています。twitter-api-v2-php、bird-elephant、TwitterOAuth です(X のツールとライブラリ(新しいタブで開きます))。3つのうち最もインストールされているのは TwitterOAuth で、Composer 経由で月に約24万回です。2026年11月1日にリリースされたバージョン 9.0.0 は v2 API だけに対応し、PHP 8.3 以降が必要です。

3つとも公式 X API のクライアントなので、呼び出しのたびに開発者アカウント、アプリのキー、前払いクレジットが必要です。新規開発者向けの無料プランはなく、X はレスポンスが返すオブジェクトごとに課金します。投稿は1件$0.005、ユーザーは1件$0.010で、投稿の読み取りは月300万件までです(X の料金表(新しいタブで開きます))。2026年11月22日からは、展開(expansions)で付随して取得される投稿者やメディアにも同じ料金がかかります(10月23日付の X の告知(新しいタブで開きます))。そのため、よくある TwitterOAuth の検索は、付いてくる投稿者の分まで課金されます。

TwitterOAuth
use Abraham\TwitterOAuth\TwitterOAuth;

$x = new TwitterOAuth($apiKey, $apiSecret, null, $bearerToken);
$posts = $x->get('tweets/search/recent', [
    'query' => 'laravel lang:en',
    'max_results' => 100,
    'expansions' => 'author_id',
]);
// 100 posts: $0.50. From Nov 22, 2026, with authors: up to $1.50

古い情報はさらに昔を指しています。この検索で Google の最初の結果は J7mbo/twitter-api-php です。v1.1 の呼び出し用のラッパーで、GitHub のリポジトリはアーカイブされており、最後の更新は2020年4月です。公式ルートの現行の料金、上限、日付はすべて X API の料金にあります。

公式ルートで最初の呼び出しの前に必要なもの

  • console.x.com の開発者アカウント。X アカウントで開設し、X の開発者契約に同意しておきます。
  • API キー、API シークレット、ベアラートークンを持つアプリ。X は一度しか表示しません(X のアクセスガイド(新しいタブで開きます))。
  • 事前に購入するクレジット。返された投稿とユーザーごとに消費されます。
  • 投稿を行う処理のための OAuth 1.0a ユーザートークン(アクセストークンとそのシークレット)。TwitterOAuth の4引数のコンストラクターが受け取ります。

公開されている投稿やプロフィールを読むだけなら、どれも要りません。次のセクションでは、キー1つと、ほとんどの PHP ビルドに最初から入っている cURL 拡張で読み取ります。

#キー1つで PHP からツイートを取得する

PHP から X データを読むには、Twitter(X)API 兼スクレイピングツールである Sorsa API を使います。OAuth の代わりに ApiKey ヘッダー1つ、無料アカウントを含む全プランで毎秒20リクエスト、タイムラインと検索は1,000ツイートあたり$0.07からです。どのレスポンスも素の JSON で、json_decode で配列にでき、どのツイートにも投稿者のプロフィール全体が含まれます。X のログインを求められることはないので、あなたの X アカウントに危険が及ぶことはありません。

  1. キーを取得する。 サインアップして、ダッシュボードから API キーをコピーします。最初の100リクエストは無料で、カード不要、有効期限もありません。

  2. キーをコードの外に置く。 環境変数に設定します(Laravel なら .env)。API の呼び出しはサーバー側の PHP からだけ行ってください。ブラウザの JavaScript にキーを入れると、訪問者全員に見えてしまいます。

    Terminal
    export SORSA_API_KEY="your_key"  # PowerShell: $env:SORSA_API_KEY="your_key"
    
  3. アカウントの最新ツイートを取得する。 ユーザー名を、ApiKey ヘッダーにキーを付けて POST /user-tweets(新しいタブで開きます) に送ります。まずはターミナルで試してみましょう。

    Terminal
    curl -X POST "https://api.sorsa.io/v3/user-tweets" \
      -H "ApiKey: YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"username": "php_release_notes"}'
    
  4. PHP から呼び出す。 素の cURL なら Composer のパッケージは不要です。Guzzle を使う場合は、base_uri の末尾にスラッシュを付け、パスの先頭にはスラッシュを付けないでください。そうしないと /v3 の部分が落ちてしまいます。

    <?php
    $key = getenv('SORSA_API_KEY') ?: 'YOUR_API_KEY';
    
    $ch = curl_init('https://api.sorsa.io/v3/user-tweets');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            'ApiKey: ' . $key,
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode(['username' => 'php_release_notes']),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 30,
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    
    $data = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
    if ($status !== 200) {
        exit("HTTP $status: {$data['message']}\n");
    }
    foreach ($data['tweets'] as $tweet) {
        echo $tweet['created_at'], ' ', mb_substr($tweet['full_text'], 0, 60), "\n";
    }
    
  5. ページを読む。 各レスポンスには最大20ツイートが入り、続きがある間は next_cursor も入ります。以下の値は、ドキュメントどおりの形式のデモデータです。

    Response200 OK
    {
      "tweets": [
        {
          "id": "1985432176540981234",
          "full_text": "PHP 8.5.12 is out with two security fixes. Upgrade today.",
          "created_at": "2026-11-01T18:42:07Z",
          "lang": "en",
          "likes_count": 42,
          "retweet_count": 7,
          "reply_count": 3,
          "quote_count": 1,
          "view_count": 3810,
          "bookmark_count": 5,
          "is_reply": false,
          "user": {
            "id": "1592041833471827968",
            "username": "php_release_notes",
            "display_name": "PHP Release Notes",
            "followers_count": 1840,
            "verified": false
          }
        }
      ],
      "next_cursor": "DAABCgABF7Y8kQ3wAAA"
    }
    

各ツイートにはカウントと投稿者が付いて届くので、PHP スクリプトで expansions やフィールド一覧を指定する必要はありません。多くのスクリプトが読むフィールドは次のとおりです。

フィールド内容
idツイートの ID。19桁で、文字列として送られる
full_text投稿の全文
created_atUTC の ISO 8601 形式の時刻。new DateTimeImmutable() にそのまま渡せる
likes_count, retweet_count, reply_count, quote_countエンゲージメント数
view_count, bookmark_count表示数とブックマーク数
user投稿者のプロフィール。username や followers_count など19フィールド
quoted_status, retweeted_status引用元またはリポスト元のツイート(同じ形式)
  • id

    内容
    ツイートの ID。19桁で、文字列として送られる
  • full_text

    内容
    投稿の全文
  • created_at

    内容
    UTC の ISO 8601 形式の時刻。new DateTimeImmutable() にそのまま渡せる
  • likes_count, retweet_count, reply_count, quote_count

    内容
    エンゲージメント数
  • view_count, bookmark_count

    内容
    表示数とブックマーク数
  • user

    内容
    投稿者のプロフィール。username や followers_count など19フィールド
  • quoted_status, retweeted_status

    内容
    引用元またはリポスト元のツイート(同じ形式)

#リトライ付きのリクエスト関数

すべての呼び出しを、リトライで直るものだけを再試行する1つの関数に通します。429 は、あなたのキーで1秒以内に20を超えるリクエストを開始したという意味で、5xx や切断された接続は一時的な障害です。どちらも時間が経てば解消します。sorsa() はこれらを最大4回、1、2、4、8秒待ってリトライし、GET パラメーターは繰り返しキーの形式で送ります。これは、後述のバッチエンドポイントが読む形式です。このガイドの以降のコードは、これを土台にしています。

sorsa.php
<?php
const SORSA_API = 'https://api.sorsa.io/v3';
const RETRY = [429, 500, 502, 503, 504];

/** Build a query string; a list becomes a repeated key: a=1&a=2. */
function query(array $params): string
{
    $parts = [];
    foreach ($params as $name => $value) {
        foreach ((array) $value as $one) {
            $parts[] = rawurlencode($name) . '=' . rawurlencode($one);
        }
    }
    return implode('&', $parts);
}

/** Send one request; retry 429, 5xx and network errors up to 4 times. */
function sorsa(string $method, string $path, array $params = []): array
{
    $url = SORSA_API . $path;
    $headers = ['ApiKey: ' . (getenv('SORSA_API_KEY') ?: 'YOUR_API_KEY')];
    $options = [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30];

    if ($method === 'GET') {
        $url .= $params ? '?' . query($params) : '';
    } else {
        $headers[] = 'Content-Type: application/json';
        $options[CURLOPT_POSTFIELDS] = json_encode($params);
    }
    $options[CURLOPT_HTTPHEADER] = $headers;

    for ($attempt = 0; ; $attempt++) {
        $ch = curl_init($url);
        curl_setopt_array($ch, $options);
        $raw = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); // 0: no answer
        $again = in_array($status, [0, ...RETRY], true);
        if (!$again || $attempt === 4) {
            break;
        }
        sleep(2 ** $attempt); // 1, 2, 4, 8 seconds
    }

    if ($raw === false) {
        throw new RuntimeException('Network error: ' . curl_error($ch));
    }
    $data = json_decode($raw, true, flags: JSON_THROW_ON_ERROR);
    if ($status !== 200) {
        throw new RuntimeException("HTTP $status: " . ($data['message'] ?? ''));
    }
    return $data;
}

401 や 403 はリトライする価値がありません。キーが間違っているか未指定であるか、リクエストを使い切っているかのどれかで、どれなのかは message フィールドに書かれています。失敗したリクエストは課金されないのでリトライに費用はかからず、Sorsa API のエンドポイントはすべて読み取りだけを行うため、POST を繰り返しても何かが二重に作られることはありません。この関数は PHP 8.0 以降で動きます。curl_close() は呼んでいません。PHP 8.0 以降は何もしない関数になっており、cURL ハンドルが自動的に閉じられるからです。

#next_cursor でページをたどる

カーソルは、結果の次のページへの不透明なキーです。next_cursor は変更せずに送り返し、レスポンスに含まれなくなったら止めます。タイムラインと POST /search-tweets(新しいタブで開きます) は1ページあたり最大20ツイートを返しますが、まだ続きがあってもページの件数が少ないことがあるため、件数で終わりを判断することはできません。ジェネレーターなら1ページずつ渡せるので、ジョブがどれだけ長くなってもメモリ使用量は一定です。

PHP
require 'sorsa.php';

/** Yield pages of a POST endpoint until the cursor runs out. */
function pages(string $path, array $body, int $maxPages = 100): Generator
{
    for ($i = 0; $i < $maxPages; $i++) {
        $page = sorsa('POST', $path, $body);
        yield $page;
        if (empty($page['next_cursor'])) {
            return;
        }
        $body['next_cursor'] = $page['next_cursor'];
    }
}

foreach (pages('/search-tweets', ['query' => 'laravel lang:en']) as $page) {
    foreach ($page['tweets'] as $tweet) {
        echo $tweet['id'], ' @', $tweet['user']['username'], "\n";
    }
}

1つのカーソルをたどる間は、クエリと order を変えないでください。$maxPages は予算として扱いましょう。100ページは100リクエストで、最大2,000ツイートです。GET エンドポイントはカーソルをクエリ文字列で受け取ります。GET /followers(新しいタブで開きます) は1ページあたり最大200プロフィールを返します。

PHP
$params = ['username' => 'php_release_notes'];
$followers = [];
do {
    $page = sorsa('GET', '/followers', $params);
    array_push($followers, ...$page['users']);
    $params['next_cursor'] = $page['next_cursor'] ?? null;
} while ($params['next_cursor']);

2つの作業には、専用の完成したスクリプトがあります。ユーザーの全ツイートをダウンロードは、保存したカーソルからタイムライン全体を再開でき、Twitter のフォロワーをエクスポートするガイドは、フォロワー一覧を CSV に書き出します。

#1リクエストで100件のツイートやプロフィールを取得する

ツイート ID やユーザー名をすでに持っているなら、バッチで送ります。POST /tweet-info-bulk(新しいタブで開きます) は最大100件のツイート ID またはリンクを受け取り、GET /info-batch(新しいタブで開きます) は最大100件のユーザー名を受け取ります。バッチのレスポンスにカーソルはありません。1回のレスポンスにバッチ全体が入っています。

PHP
$tweets = [];
foreach (array_chunk($tweetIds, 100) as $batch) {
    $res = sorsa('POST', '/tweet-info-bulk', ['tweet_links' => $batch]);
    array_push($tweets, ...$res['tweets']);
}

$profiles = [];
foreach (array_chunk($usernames, 100) as $batch) {
    $res = sorsa('GET', '/info-batch', ['usernames' => $batch]);
    array_push($profiles, ...$res['users']);
}

リクエストは何を返しても同じ料金なので、100ツイートのバッチも1ツイートと同じコストです。1,000ツイートあたり$0.014から、公式 X API の$5.00の250分の1以下です。この方法なら、100回分の無料リクエストで1万ツイートをカバーできます。

知っておきたいこと

GET /info-batch は、リストを繰り返しキー(usernames=a&usernames=b)として読みます。これは OpenAPI リファレンスが定める形式です。PHP の http_build_query() は代わりに usernames%5B0%5D=a&usernames%5B1%5D=b を書き出します。Guzzle の配列形式の query オプションや、Laravel の Http::get($url, $array) も同じ方法でクエリを組み立てるので同様です。上の query() ヘルパーか、繰り返しキーを書き出す GuzzleHttp\Psr7\Query::build() を使ってください。

$params = ['usernames' => ['php_release_notes', 'laravel_weekly_digest']]; 角括弧の名前 または、Guzzleのqueryオプション usernames%5B0%5D=php_release_notes &usernames%5B1%5D=laravel_weekly_digest 名前がusernames[0]とusernames[1]の2パラメータ。PHP独自の命名の癖 キーの繰り返し または usernames=php_release_notes &usernames=laravel_weekly_digest 値が2つのusernamesパラメータ1つ。GET /info-batchが定めている形式
PHP の既定では角括弧付きの名前が書かれる。バッチエンドポイントは繰り返された1つの名前を読むので、そのクエリ文字列は自分で組み立てること。

#毎秒19リクエストでバッチを並列実行する

PHP は curl_multi でリクエストを並行して実行できます。作業が独立したリクエストに分かれていれば、プロセス1つで Sorsa API のキーあたり毎秒20リクエストに届きます。バッチはそれぞれが独立しているので、きれいに分けられます。カーソルのチェーンは分けられません。各ページが直前のページのカーソルを必要とするからです。その場合は、アカウントごと、または期間ごとに別々のチェーンを並べて実行してください。parallel() は毎秒最大19リクエストまでしか開始せず、上限より1つ低く保ち、429 や 5xx は順番待ちの列に戻します。

PHP
/** POST many bodies side by side, starting at most $rate a second. */
function parallel(string $path, array $bodies, float $rate = 19): array
{
    $multi = curl_multi_init();
    $queue = array_keys($bodies);
    $tries = $results = [];
    $next = microtime(true);
    $running = 0;

    while ($queue || $running) {
        if ($queue && microtime(true) >= $next) {
            $key = array_shift($queue);
            $ch = curl_init(SORSA_API . $path);
            curl_setopt_array($ch, [
                CURLOPT_HTTPHEADER => [
                    'ApiKey: ' . (getenv('SORSA_API_KEY') ?: 'YOUR_API_KEY'),
                    'Content-Type: application/json',
                ],
                CURLOPT_POSTFIELDS => json_encode($bodies[$key]),
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT => 30,
                CURLOPT_PRIVATE => (string) $key,
            ]);
            curl_multi_add_handle($multi, $ch);
            $next = max($next, microtime(true) - 1 / $rate) + 1 / $rate;
        }
        curl_multi_exec($multi, $running);
        while ($done = curl_multi_info_read($multi)) {
            $ch = $done['handle'];
            $key = curl_getinfo($ch, CURLINFO_PRIVATE);
            $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
            $tries[$key] = ($tries[$key] ?? 0) + 1;
            if (in_array($status, [0, ...RETRY], true) && $tries[$key] < 5) {
                $queue[] = $key; // back in line, it waits its turn
            } else {
                $results[$key] = json_decode(curl_multi_getcontent($ch), true);
            }
            curl_multi_remove_handle($multi, $ch);
        }
        curl_multi_select($multi, 0.01);
    }
    return $results;
}

$bodies = array_map(fn ($batch) => ['tweet_links' => $batch],
    array_chunk($tweetIds, 100));
$tweets = array_merge(...array_column(parallel('/tweet-info-bulk', $bodies),
    'tweets'));
parallel('/tweet-info-bulk', $bodies) ツイートID 100件のバッチを100個 1つのAPIキーでの1秒間 各バーは処理中のリクエスト1件 0 s 1 s 19 毎秒開始数、 上限より1つ少ない キーあたり20 毎秒1,900ツイート ID 100件のバッチ19個 1万ツイートで約5秒 リクエスト100件、毎秒19件ずつ開始 429または5xx: 列の最後尾へ 空き枠まで待機 ループは開始時刻が来たときだけリクエストを追加します。先のレスポンスにどれだけ時間がかかっても同じです。
開始は固定のクロックで行われるので、応答が遅くても速くても、キーが毎秒20リクエストを超えることはない。

毎秒19回の開始なら、100バッチで約5秒、毎秒1,900ツイートの速さで1万ツイートを処理できます。キー1つにつき parallel() のループは1つだけにしてください。2つのプロセスを並べて動かすと、両者の合計で上限の2倍の開始になります。Guzzle の Pool はこの用途に向いているように見えますが、その concurrency オプションが制限するのは毎秒の開始数ではなく同時に実行中のリクエスト数です。そのため、応答が速いと毎秒20を超えてしまい、リトライに頼ることになります。

#Laravel:Http クライアントで同じ呼び出しを行う

Laravel の Http クライアントは Guzzle のラッパーなので、Laravel アプリに追加のパッケージは要りません。マクロを1つ定義して、ベース URL、キー、そして 429、5xx のレスポンス、切断された接続だけを繰り返すリトライのルールを設定します。あとはどのコントローラー、ジョブ、Artisan コマンドからでも Http::sorsa() を呼び出せます。

// config/services.php: 'sorsa' => ['key' => env('SORSA_API_KEY')],
// .env: SORSA_API_KEY=YOUR_API_KEY

// app/Providers/AppServiceProvider.php
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;

// inside the class
public function boot(): void
{
    Http::macro('sorsa', fn () => Http::baseUrl('https://api.sorsa.io/v3')
        ->withHeaders(['ApiKey' => config('services.sorsa.key')])
        ->timeout(30)
        ->retry(5, fn (int $attempt) => 1000 * 2 ** ($attempt - 1),
            fn (Throwable $e) => $e instanceof ConnectionException
                || ($e instanceof RequestException
                    && ($e->response->status() === 429
                        || $e->response->serverError()))));
}

retry() は試行の間に1、2、4、8秒待ち、試行を使い切った場合や、401 や 403 のようにリトライする価値のないステータスだった場合に RequestException を投げます。そのため、失敗した呼び出しがキャッシュに入ることはありません。数千件のバッチリクエストのような長いジョブは、ページのリクエストの中ではなく、キューに入れたジョブか Artisan コマンドから実行してください。

キャッシュしたフィードが消費するリクエストは、訪問者ではなく時間で決まります。次のプランナーは、フィードの数とキャッシュ時間から、1日と1か月のリクエスト数と、それを収められる Sorsa API のプランを割り出します。

サイトのフィード数(アカウントまたは検索)
各フィードのキャッシュ時間
1日のリクエスト数
288

トラフィックに関係なく

月のリクエスト数
8,640

30日間

収まるSorsa APIのプラン
Starter

月10Kリクエスト、86%使用

無料の100リクエストが持つ期間
約 8 h

カード不要、有効期限なし

同じ5分間は、訪問者全員がキャッシュ済みのコピーを読むため、100ページビューでも100万ページビューでも、コストは同じ1日288リクエストです。

リクエスト数はキャッシュの更新頻度で決まり、ページビューには左右されません。

#PHP のジョブのコスト

Sorsa API はエンドポイントが何を返すかにかかわらず、リクエスト単位で課金します。タイムラインと検索は1,000ツイートあたり$0.07から、バッチは$0.014からで、失敗したリクエストは課金されません。公式 X API はオブジェクト単位で課金します。1,000投稿あたり$5.00で、2026年11月22日からは、ほとんどの TwitterOAuth の検索のように各投稿の投稿者を展開すると、1,000投稿あたり最大$15.00になります。

PHP のコードが収集するツイート数

公式 X API

$5,000

1Mツイートの場合

1投稿$0.005

Sorsa API

$70から

1Mツイートの場合

1,000件あたり$0.07から

投稿者のプロフィールは、すべてのツイートに含まれています

1,000リクエストあたりの単価は、Starter の$4.90から Hyperscale の$1.40までで、どのプランもすべてのエンドポイントに同じ単価を適用します。残りの確認は無料で行えます。GET /key-usage-info(新しいタブで開きます) はリクエストとしてカウントされません。

PHP
$usage = sorsa('GET', '/key-usage-info');
echo $usage['remaining_requests'], ' left until ', $usage['valid_until'], "\n";

#TwitterOAuth のコードを Sorsa API へ移す

TwitterOAuth の読み取りの大半は Sorsa API のエンドポイント1つに対応し、すべてのツイートが最初から投稿者とカウントを持つため、コードから expansions とフィールド一覧がなくなります。次の表は、よく使う v2 のパスとエンドポイントを対応させたものです。コードベース全体を移すなら、Twitter/X API から移行するガイドが v2 の全エンドポイントとレスポンスのフィールドを対応づけています。

TwitterOAuth の呼び出しSorsa API のエンドポイント変わる点
get('tweets/search/recent')POST /search-tweets直近の検索とアーカイブ検索が1つのエンドポイントで、2006年までさかのぼれる
get("users/$id/tweets")POST /user-tweetsユーザー名だけでよく、3,200件の上限がない
get('tweets') と idsPOST /tweet-info-bulk1リクエストあたり100件の ID またはリンク
get('users/by') と usernamesGET /info-batch1リクエストあたり100ユーザー名
get("users/$id/followers")GET /followers1ページあたり200フォロワー
post('tweets')なし読み取り専用:投稿は公式 X API で
  • get('tweets/search/recent')

    Sorsa API のエンドポイント
    POST /search-tweets
    変わる点
    直近の検索とアーカイブ検索が1つのエンドポイントで、2006年までさかのぼれる
  • get("users/$id/tweets")

    Sorsa API のエンドポイント
    POST /user-tweets
    変わる点
    ユーザー名だけでよく、3,200件の上限がない
  • get('tweets') と ids

    Sorsa API のエンドポイント
    POST /tweet-info-bulk
    変わる点
    1リクエストあたり100件の ID またはリンク
  • get('users/by') と usernames

    Sorsa API のエンドポイント
    GET /info-batch
    変わる点
    1リクエストあたり100ユーザー名
  • get("users/$id/followers")

    Sorsa API のエンドポイント
    GET /followers
    変わる点
    1ページあたり200フォロワー
  • post('tweets')

    Sorsa API のエンドポイント
    なし
    変わる点
    読み取り専用:投稿は公式 X API で

フィールド名も変わります。text は full_text に、public_metrics->like_count は likes_count に、author_id と includes->users は user 配列に、meta->next_token は next_cursor になります。TwitterOAuth はオブジェクトを返しますが、json_decode($body, true) ならこのガイドで使っている配列になります。ユーザーのタイムラインも、公式のタイムラインが止まる(新しいタブで開きます)最新3,200件の投稿を越えて取得できます。

#今日、最初の PHP リクエストを送る

PHP で X データを読むのに必要なのは、cURL 拡張、キー1つ、そして4つの短い関数です。リトライ用の sorsa()、カーソル用の pages()、バッチエンドポイントと組み合わせて1リクエスト100件にするための array_chunk()、毎秒19リクエストのための parallel() です。まずは、公開された X データを大規模に読むために作られた Twitter API である Sorsa API から始めましょう。タイムラインや検索は1リクエスト20ツイートで1,000件あたり$0.07から、バッチは1回100件で$0.014から、100回分の無料リクエストはカード不要で期限もありません。ほかの言語での同じ呼び出しは、Python での Twitter API と Node.js での Twitter API のガイドにあります。

#よくある質問

PHP ではどの Twitter API を使うべきですか?

PHP から X データを読むなら Sorsa API を使います。OAuth の代わりに ApiKey ヘッダーのキー1つ、json_decode でそのまま扱える素の JSON、タイムラインと検索は1,000ツイートあたり$0.07から、バッチは$0.014から、全プランで毎秒20リクエスト、検索は2006年までさかのぼれます。X へのログインは不要なので、あなたのアカウントが危険にさらされることはありません。投稿も行いたい場合は、公式 X API と組み合わせてください。

PHP 向けの無料の Twitter API はありますか?

公式 X API に、新規開発者向けの無料プランはありません。TwitterOAuth 自体は無料ですが、読み取りのたびに前払いクレジットが投稿1件あたり$0.005消費されます。Sorsa API は、すべてのアカウントに、期限がなくカードも不要な100回分の無料リクエストを付けています。PHP からなら、タイムラインや検索で2,000ツイート、100件のバッチなら最大1万ツイートを取得できます。全体像は Twitter API は無料かにあります。

Twitter(X)は PHP を使っていますか?

いいえ。Twitter は Ruby on Rails で始まり、2010年からバックエンドを JVM に移して、サービスは Scala と Java で書かれています。Twitter のエンジニアリングブログ(新しいタブで開きます)が説明しているとおりです。X が2023年と2026年1月に公開したレコメンデーションのコードも、大半が Scala、Java、Rust、Python で、PHP はまったく含まれていません。それでも PHP はクライアントとして問題なく機能します。X のデータは、HTTPS 経由の JSON として PHP スクリプトに届くからです。

Sorsa API で PHP からツイートを投稿できますか?

できません。Sorsa API は設計上、読み取り専用です。公開された投稿、プロフィール、フォロワーを読み取るだけで、投稿、いいね、フォロー、メッセージ送信は行わないため、キーで X アカウントを操作することはできません。PHP から投稿するには、OAuth 1.0a ユーザートークンを使った公式 X API を使います。たとえば TwitterOAuth の post('tweets', ...) は1投稿$0.015です。読み取りには Sorsa API を使い続けてください。

WordPress で Sorsa API を使えますか?

使えます。WordPress 標準の HTTP 関数で同じリクエストを送れます。ApiKey ヘッダーと JSON ボディを付けて wp_remote_post() を呼び、wp_remote_retrieve_body() と json_decode() で結果を取り出します。結果は set_transient() で数分間保存して、ページビューがそのままリクエストにならないようにしてください。キーは wp-config.php か環境変数に置き、ブラウザが読み込むスクリプトには決して入れないでください。

Sorsa API 向けの Composer パッケージはありますか?

パッケージは不要です。Sorsa API は JSON を使う素の REST なので、cURL 拡張、Guzzle、Laravel の Http クライアントで45あるエンドポイントすべてをカバーでき、このガイドの関数は約100行に収まります。api.sorsa.io/v3/swagger.json の OpenAPI リファレンスにすべてのエンドポイントが載っているので、必要なら openapi-generator のようなジェネレーターで型付きの PHP クライアントを生成できます。

#検証方法

このガイドのすべてのリクエストを、Sorsa API の最新の OpenAPI リファレンス(45エンドポイント)と、レート制限とページネーションに関するドキュメントから書きました。いずれも2026年11月2日に確認しています。今回は Sorsa API のキーがなかったため、表示したレスポンスはデモ値です。PHP のブロックはすべて、PHP 8.4.27、Guzzle 8.3.1、Laravel の illuminate/http 13 で、ドキュメントどおりの形式で応答し、毎秒20リクエストを超えると 429 を返すローカルのモックに対して実行しました。最初のリクエスト、リトライ、2つのカーソルのループ、100件のバッチ、クエリ文字列、Laravel のマクロ、そして parallel() です。後者は毎秒19件で100件のバッチリクエストを送り、1回の実行あたり 429 は多くても1件で、そのたびにリトライで回復しました。同じ日に、X の料金表、ツールのページ、タイムラインのドキュメント、expansions の告知、そして Packagist にある TwitterOAuth、Guzzle、Laravel のリリースを確認しました。

Sorsa APIチーム 読み取り専用のX(Twitter)APIとスクレイピングツール、Sorsa APIを開発・運営しています。2022年から本番稼働し(旧TweetScout API)、累計60億リクエスト以上を処理しています。 Sorsa APIについて

変更点
  • 全面的に書き直しました。11月22日開始の展開(expansions)課金を含む現行の公式 X API の料金、TwitterOAuth 9.0.0、そしてキー1つで使う PHP の方法を反映しています。cURL と Guzzle のリクエスト、リトライ、カーソル、繰り返しクエリキーを使った100件のバッチ、curl_multi による毎秒19リクエストの並列実行、キャッシュ付きの Laravel の Http クライアント、TwitterOAuth のメソッド対応表を扱います。
  • 初公開。