Skip to content

Twitter API in PHP: get X data with cURL, Guzzle and Laravel

To get Twitter/X data in PHP, send HTTPS requests with cURL or Guzzle, put the API key in a header, decode the JSON and follow the cursor page by page; the official X API bills $0.005 per post.

Short answer: X ships official SDKs only for Python and TypeScript, so PHP reaches the official X API through community clients like TwitterOAuth, a developer account and prepaid credits: $0.005 per post, $0.015 with its author from November 22, 2026. For public data, use Sorsa API: one key, 20 tweets per request from $0.07 per 1,000; 100 free requests cover 2,000.

Endpoint
POST /user-tweets
Page size
20 tweets per request
Price
from $0.07 per 1,000 tweets

#Is there an official X API SDK for PHP?

No. X publishes official SDKs for Python and TypeScript only, and its tools page lists three community PHP clients that X doesn't maintain: twitter-api-v2-php, bird-elephant and TwitterOAuth (X's tools and libraries (opens in a new tab)). TwitterOAuth is the most installed of the three, about 240K Composer installs a month. Its version 9.0.0, released on November 1, 2026, speaks only the v2 API and needs PHP 8.3 or later.

All three are clients of the official X API, so every call they make needs a developer account, an app's keys and prepaid credits. New developers get no free tier, and X bills each object a response returns: $0.005 per post and $0.010 per user, up to 3M post reads a month (X's price table (opens in a new tab)). From November 22, 2026 the authors and media pulled in through expansions are billed at the same rates (X's notice of October 23 (opens in a new tab)), so the usual TwitterOAuth search pays for every author it attaches:

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

Older answers point further back. Google's first result for this search is J7mbo/twitter-api-php, a wrapper for v1.1 calls whose GitHub repository is archived and was last changed in April 2020. Every current rate, cap and date of the official route is in X API pricing.

What the official route needs before the first call

  • A developer account at console.x.com, opened with an X account and X's Developer Agreement accepted.
  • An app with its API key, API secret and bearer token, which X shows only once (X's access guide (opens in a new tab)).
  • Credits bought in advance and spent per post and per user returned.
  • OAuth 1.0a user tokens, an access token and its secret, for anything that posts; TwitterOAuth's four-argument constructor takes them.

None of it is needed to read public posts and profiles. The next section does that with one key and the cURL extension that most PHP builds already include.

#Get tweets in PHP with one API key

To read X data from PHP, use Sorsa API, a Twitter (X) API and scraper: one ApiKey header instead of OAuth, 20 requests per second on every plan, free accounts included, and timelines and search from $0.07 per 1,000 tweets. Every response is plain JSON that json_decode turns into arrays, with the author's full profile inside every tweet. It never asks for your X login, so your X account carries no risk.

  1. Get a key. Sign up and copy your API key from the dashboard. The first 100 requests are free, need no card and never expire.

  2. Keep the key out of the code. Set it as an environment variable, or in .env for Laravel, and call the API only from server-side PHP: a key in browser JavaScript is visible to every visitor.

    Terminal
    export SORSA_API_KEY="your_key"  # PowerShell: $env:SORSA_API_KEY="your_key"
    
  3. Fetch an account's latest tweets. Send the username to POST /user-tweets (opens in a new tab) with the key in the ApiKey header. Try it from the terminal first:

    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. Call it from PHP. Plain cURL needs no Composer package. With Guzzle, end base_uri with a slash and start the path without one, or the /v3 part is dropped.

    <?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. Read the page. Each response holds up to 20 tweets and a next_cursor while more remain. The values below are demo data in the documented shape.

    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"
    }
    

Each tweet arrives with its counts and its author, so a PHP script sets no expansions or field lists. These are the fields most scripts read:

FieldWhat it holds
idThe tweet's ID: 19 digits, sent as a string
full_textThe whole text of the post
created_atThe time in ISO 8601, UTC, ready for new DateTimeImmutable()
likes_count, retweet_count, reply_count, quote_countEngagement counts
view_count, bookmark_countViews and bookmarks
userThe author's profile: 19 fields such as username and followers_count
quoted_status, retweeted_statusThe quoted or reposted tweet, in the same shape
  • id

    What it holds
    The tweet's ID: 19 digits, sent as a string
  • full_text

    What it holds
    The whole text of the post
  • created_at

    What it holds
    The time in ISO 8601, UTC, ready for new DateTimeImmutable()
  • likes_count, retweet_count, reply_count, quote_count

    What it holds
    Engagement counts
  • view_count, bookmark_count

    What it holds
    Views and bookmarks
  • user

    What it holds
    The author's profile: 19 fields such as username and followers_count
  • quoted_status, retweeted_status

    What it holds
    The quoted or reposted tweet, in the same shape

#One request function with retries

Send every call through one function that retries only what a retry can fix. A 429 means more than 20 requests started within one second on your key, and a 5xx or a dropped connection is a temporary fault; both clear on their own. sorsa() retries them up to four times, waiting 1, 2, 4 and 8 seconds, and sends GET parameters as repeated keys, the form the batch endpoints below read. The rest of this guide builds on it.

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;
}

A 401 or a 403 is never worth a retry: the key is wrong or missing, or its requests are used up, and the message field says which. Failed requests are never charged, so a retry costs nothing, and every Sorsa API endpoint only reads, so repeating a POST can't create anything twice. The function runs on PHP 8.0 and later. It skips curl_close(), which has done nothing since PHP 8.0, because cURL handles now close themselves.

#Page through results with next_cursor

A cursor is an opaque key to the next page of results: send next_cursor back unchanged and stop when a response comes without one. Timelines and POST /search-tweets (opens in a new tab) return up to 20 tweets per page, and a page can hold fewer while more remain, so the item count never tells you the end has come. A generator hands out one page at a time and keeps memory flat however long the job runs.

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";
    }
}

Keep the query and order unchanged while one cursor runs, and treat $maxPages as a budget: 100 pages is 100 requests and at most 2,000 tweets. GET endpoints take the cursor in the query string instead. GET /followers (opens in a new tab) returns up to 200 profiles a page:

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']);

Two jobs have complete scripts of their own: download all tweets from a user resumes a whole timeline from a saved cursor, and the guide to export Twitter followers writes follower lists to CSV.

#Fetch 100 tweets or profiles per request

When you already hold tweet IDs or usernames, send them in batches. POST /tweet-info-bulk (opens in a new tab) takes up to 100 tweet IDs or links, and GET /info-batch (opens in a new tab) takes up to 100 usernames. Batch responses have no cursor: one response holds the whole batch.

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']);
}

A request costs the same whatever it returns, so a batch of 100 tweets costs what one tweet costs: from $0.014 per 1,000 tweets, more than 250x less than the official X API's $5.00. The 100 free requests cover 10K tweets this way.

Good to know

GET /info-batch reads a list as a repeated key, usernames=a&usernames=b, the form its OpenAPI reference defines. PHP's http_build_query() writes usernames%5B0%5D=a&usernames%5B1%5D=b instead, and so do Guzzle's array query option and Laravel's Http::get($url, $array), which build the query the same way. Use the query() helper above, or GuzzleHttp\Psr7\Query::build(), which writes repeated keys.

$params = ['usernames' => ['php_release_notes', 'laravel_weekly_digest']]; BRACKET NAMES http_build_query($params), or Guzzle's query option usernames%5B0%5D=php_release_notes &usernames%5B1%5D=laravel_weekly_digest Two parameters named usernames[0] and usernames[1], PHP's own naming habit REPEATED KEY query($params), or GuzzleHttp\Psr7\Query::build($params) usernames=php_release_notes &usernames=laravel_weekly_digest One parameter, usernames, with two values: the form GET /info-batch defines
PHP's default writes bracket names; the batch endpoint reads one repeated name, so build that query string yourself.

#Run batches in parallel at 19 requests a second

PHP runs requests side by side with curl_multi, and one process reaches Sorsa API's 20 requests per second per key once the work splits into independent requests. Batches split cleanly, because each one stands alone. A cursor chain can't be split, since every page needs the cursor from the page before, so run separate accounts or date windows side by side instead. parallel() starts at most 19 requests a second, one under the limit, and puts any 429 or 5xx back in line.

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) 100 batches of 100 tweet IDs One second on one API key each bar is one request in flight 0 s 1 s 19 starts a second, one under the limit of 20 per key 1,900 tweets a second 19 batches of 100 IDs About 5 s for 10K tweets 100 requests, 19 starting each second 429 or 5xx: back in line it waits for a free start The loop adds a request only when its start time comes, however long earlier responses take.
Starts come on a fixed clock, so slow or fast responses never push the key past 20 requests a second.

At 19 starts a second, 100 batches move 10K tweets in about 5 seconds, 1,900 tweets a second. Keep one parallel() loop per key: two processes running it side by side start twice the limit between them. Guzzle's Pool looks like the tool for this, but its concurrency option caps requests in flight, not starts per second, so a pool of quick responses can still cross 20 a second and lean on retries.

#Laravel: the same calls with the Http client

Laravel's Http client wraps Guzzle, so a Laravel app needs no extra package. One macro sets the base URL, the key and a retry rule that repeats only 429s, 5xx responses and dropped connections; every controller, job or Artisan command then calls 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() sleeps 1, 2, 4 and 8 seconds between attempts and throws a RequestException when the attempts run out or the status isn't worth a retry, such as a 401 or a 403, so a failed call never lands in the cache. Run long jobs, such as thousands of batch requests, from a queued job or an Artisan command rather than inside a page request.

A cached feed spends requests by the clock, not by the visitor. The planner turns the number of feeds and the cache time into requests a day and a month, and the Sorsa API plan that holds them.

Feeds on your site (accounts or searches)
Cache each feed for
Requests a day
288

whatever the traffic

Requests a month
8,640

30 days

Sorsa API plan that holds it
Starter

10K requests a month, 86% used

100 free requests last
about 8 h

no card, they never expire

Every visitor in the same 5 minutes reads the cached copy, so 100 page views and 1M page views cost the same 288 requests a day.

Requests depend on how often the cache refreshes, never on page views.

#What a PHP job costs

Sorsa API charges per request, whatever the endpoint returns: timelines and search cost from $0.07 per 1,000 tweets, batches from $0.014, and failed requests are never charged. The official X API charges per object instead: $5.00 per 1,000 posts, and from November 22, 2026 up to $15.00 per 1,000 when the code expands each post's author, as most TwitterOAuth searches do.

Tweets your PHP code collects

Official X API

$5,000

for 1M tweets

$0.005 per post

Sorsa API

from$70

for 1M tweets

from $0.07 per 1,000

The author's profile comes inside every tweet

The rate per 1,000 requests runs from $4.90 on Starter to $1.40 on Hyperscale, and each plan applies it to every endpoint alike. Checking what is left costs nothing: GET /key-usage-info (opens in a new tab) never counts against your requests.

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

#Moving TwitterOAuth code to Sorsa API

Most TwitterOAuth reads map to one Sorsa API endpoint, and the code drops its expansions and field lists, because every tweet already carries its author and counts. The table pairs common v2 paths with their endpoints; to move a whole codebase, the guide to migrate from the Twitter/X API maps every v2 endpoint and response field.

TwitterOAuth callSorsa API endpointWhat changes
get('tweets/search/recent')POST /search-tweetsOne endpoint for recent and archive search, back to 2006
get("users/$id/tweets")POST /user-tweetsA username is enough, and there is no 3,200-post cap
get('tweets') with idsPOST /tweet-info-bulk100 IDs or links per request
get('users/by') with usernamesGET /info-batch100 usernames per request
get("users/$id/followers")GET /followers200 followers per page
post('tweets')noneRead-only: post through the official X API
  • get('tweets/search/recent')

    Sorsa API endpoint
    POST /search-tweets
    What changes
    One endpoint for recent and archive search, back to 2006
  • get("users/$id/tweets")

    Sorsa API endpoint
    POST /user-tweets
    What changes
    A username is enough, and there is no 3,200-post cap
  • get('tweets') with ids

    Sorsa API endpoint
    POST /tweet-info-bulk
    What changes
    100 IDs or links per request
  • get('users/by') with usernames

    Sorsa API endpoint
    GET /info-batch
    What changes
    100 usernames per request
  • get("users/$id/followers")

    Sorsa API endpoint
    GET /followers
    What changes
    200 followers per page
  • post('tweets')

    Sorsa API endpoint
    none
    What changes
    Read-only: post through the official X API

Field names change as well: text becomes full_text, public_metrics->like_count becomes likes_count, author_id with includes->users becomes the user array, and meta->next_token becomes next_cursor. TwitterOAuth returns objects, while json_decode($body, true) gives the arrays this guide uses. The user timeline also reaches past the newest 3,200 posts, where the official timeline stops (opens in a new tab).

#Send your first PHP request today

Reading X data in PHP takes the cURL extension, one key and four short functions: sorsa() for retries, pages() for cursors, array_chunk() with the batch endpoints for 100 items per request, and parallel() for 19 requests a second. Start with Sorsa API, the Twitter API built for reading public X data at scale: 20 tweets per timeline or search request from $0.07 per 1,000, 100 per batch from $0.014, and 100 free requests that need no card and never expire. The same calls in other languages are in the guides to the Twitter API in Python and the Twitter API in Node.js.

#FAQ

Which Twitter API should I use with PHP?

For reading X data from PHP, use Sorsa API: one key in an ApiKey header instead of OAuth, plain JSON for json_decode, timelines and search from $0.07 per 1,000 tweets and batches from $0.014, 20 requests per second on every plan, search back to 2006, and no X login, so your account is never at risk. To post as well, pair it with the official X API.

Is there a free Twitter API for PHP?

The official X API has no free tier for new developers: TwitterOAuth is free, but every read spends prepaid credits at $0.005 per post. Sorsa API starts every account with 100 free requests that never expire and need no card. From PHP they fetch 2,000 tweets through timelines or search, or up to 10K tweets in batches of 100. The full picture is in is the Twitter API free.

Does Twitter (X) use PHP?

No. Twitter launched on Ruby on Rails and from 2010 moved its back end to the JVM, with services in Scala and Java, as Twitter's engineering blog (opens in a new tab) describes. The recommendation code X published in 2023 and again in January 2026 is mostly Scala, Java, Rust and Python, with no PHP at all. PHP works well as a client all the same: X data reaches a PHP script as JSON over HTTPS.

Can I post tweets from PHP with Sorsa API?

No. Sorsa API is read-only by design: it reads public posts, profiles and followers and never posts, likes, follows or sends messages, so a key can't act on any X account. To post from PHP, use the official X API with OAuth 1.0a user tokens, such as TwitterOAuth's post('tweets', ...) at $0.015 a post, and keep Sorsa API for the reads.

Can I use Sorsa API in WordPress?

Yes. WordPress's own HTTP functions send the same request: wp_remote_post() with the ApiKey header and a JSON body, then wp_remote_retrieve_body() and json_decode(). Keep the result with set_transient() for a few minutes, so page views never turn into requests, and store the key in wp-config.php or an environment variable, never in a script the browser loads.

Is there a Composer package for Sorsa API?

No package is needed: Sorsa API is plain REST with JSON, so the cURL extension, Guzzle or Laravel's Http client covers all 45 endpoints, and the functions in this guide fit in about 100 lines. The OpenAPI reference at api.sorsa.io/v3/swagger.json lists every endpoint, so a generator such as openapi-generator can build a typed PHP client if you want one.

#How we checked

We wrote every request in this guide from Sorsa API's live OpenAPI reference (45 endpoints) and its docs on rate limits and pagination, read on November 2, 2026. We had no Sorsa API key for this pass, so the response shown uses demo values, and we ran each PHP block on PHP 8.4.27 with Guzzle 8.3.1 and Laravel's illuminate/http 13 against a local mock that answers in the documented shapes and returns 429 past 20 requests a second: the first requests, retries, both cursor loops, batches of 100, the query strings, the Laravel macro, and parallel(), which sent 100 batch requests at 19 a second with at most one 429 per run, always recovered by its retry. The same day we read X's price table, tools page, timeline docs and expansions notice, and the TwitterOAuth, Guzzle and Laravel releases on Packagist.

By the Sorsa API team. We build and run Sorsa API, the read-only Twitter/X API and scraper, in production since 2022 (formerly TweetScout API) with 6B+ requests served. About Sorsa API

What changed
  • Rewritten from scratch: today's official X API prices with the November 22 expansion billing, TwitterOAuth 9.0.0, and a PHP path with one API key: cURL and Guzzle requests, retries, cursors, batches of 100 with repeated query keys, parallel requests at 19 a second with curl_multi, Laravel's Http client with caching, and a TwitterOAuth method map.
  • First published.