Skip to content

PHP

Twitter API PHP client

The PHP client for TwitterAPIs is the built-in curl extension and json_decode, both shipped with most PHP builds. Authentication is a single static header, so there is no credential machinery to build and no Composer package to add. One method on a small class covers every read endpoint, with a typed array result and an explicit timeout you control.

No dependency. curl ships with most PHP builds.

twitterapis.php
1<?php
2const BASE = "https://api.twitterapis.com";
3
4function twitterapis_get(string $path, array $params = []): array {
5 $url = BASE . "/twitter/" . $path . "?" . http_build_query($params);
6 $ch = curl_init($url);
7 curl_setopt_array($ch, [
8 CURLOPT_RETURNTRANSFER => true,
9 CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("TWITTERAPIS_KEY")],
10 CURLOPT_TIMEOUT => 30,
11 ]);
12 $body = curl_exec($ch);
13 $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
14 curl_close($ch);
15 if ($status !== 200) {
16 throw new RuntimeException("twitterapis: status $status");
17 }
18 return json_decode($body, true);
19}
20
21$profile = twitterapis_get("user/info", ["username" => "naval"]);
22echo $profile["user"]["id"], " ", $profile["user"]["followers_count"], PHP_EOL;

A reusable client class

Wrapping the key and a timeout in a class gives you one place to configure both, one place to add retry logic later, and a single get() method every call site shares instead of repeating the curl boilerplate.

TwitterApisClient.php
1final class TwitterApisClient
2{
3 private string $key;
4 private int $timeout;
5
6 public function __construct(string $key, int $timeout = 30)
7 {
8 $this->key = $key;
9 $this->timeout = $timeout;
10 }
11
12 public function get(string $path, array $params = []): array
13 {
14 $url = "https://api.twitterapis.com/twitter/" . $path
15 . "?" . http_build_query($params);
16
17 $ch = curl_init($url);
18 curl_setopt_array($ch, [
19 CURLOPT_RETURNTRANSFER => true,
20 CURLOPT_HTTPHEADER => ["Authorization: Bearer " . $this->key],
21 CURLOPT_TIMEOUT => $this->timeout,
22 ]);
23 $body = curl_exec($ch);
24 if ($body === false) {
25 $err = curl_error($ch);
26 curl_close($ch);
27 throw new RuntimeException("twitterapis: transport error: $err");
28 }
29 $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
30 curl_close($ch);
31
32 if ($status !== 200) {
33 throw new RuntimeException("twitterapis: status $status: $body");
34 }
35 return json_decode($body, true, flags: JSON_THROW_ON_ERROR);
36 }
37}

Walking a cursor-paginated endpoint

A do-while loop reads next_cursor from each page and feeds it back as the next request's cursor parameter, stopping when it comes back empty. The callback lets the caller abort early, which matters when each page is a separate billed call.

followers.php
1/**
2 * Walks every page of a cursor-paginated endpoint, calling $fn once per page.
3 * Each page is one billed call, so the caller controls the loop by having
4 * $fn return false to stop early.
5 */
6function followers(TwitterApisClient $client, string $username, callable $fn): void
7{
8 $cursor = null;
9 do {
10 $params = ["username" => $username];
11 if ($cursor !== null) {
12 $params["cursor"] = $cursor;
13 }
14 $page = $client->get("user/followers", $params);
15 if ($fn($page) === false) {
16 return;
17 }
18 $cursor = $page["next_cursor"] ?? "";
19 } while ($cursor !== "");
20}

Where to go next

The same client shape in Python and Go, or all six languages on the language clients page. The REST API reference covers the paging model and the status codes this client branches on, and the rate limits page covers what concurrency you can actually run.

Frequently Asked Questions

There is no Composer package to install here, and the client above shows why one would add little. curl handles the request, json_decode handles the response, and the only credential work is setting a single header. That is the entire surface a package would wrap, so writing it yourself gives you a client you can read in one file and change without waiting on a maintainer.

They are two different failure modes and curl reports them differently. curl_exec returns false on a transport failure (DNS, TLS, connection reset, timeout), which curl_error explains. A non-200 HTTP status is a successful transport with an application-level rejection, read from curl_getinfo's CURLINFO_RESPONSE_CODE. Check both, in that order, or a transport failure gets misread as a bad response body.

Yes, with curl_multi_init, which runs several handles on one event loop without needing threads or an async framework. There is one ceiling to design around, 600 requests a minute and 20 concurrent per key, so cap the multi-handle's running count at or under 20 rather than adding every request at once.

Set CURLOPT_TIMEOUT on the handle, in seconds, before calling curl_exec. It bounds the whole request including DNS and TLS setup. If you need to bound just the connect phase separately, CURLOPT_CONNECTTIMEOUT does that; most single-region API calls only need the one overall timeout.

Read next_cursor from each response and pass it as the cursor parameter on the next request, stopping when it comes back empty. The helper on this page uses a do-while loop with a callback so the caller can stop early, which matters when each page is a separate billed call and a large account has a lot of them.

$0.0008 for a standard read, the same everywhere, billed per call rather than per plan. The client makes exactly one request per get(), so your bill is the number of times your code calls it. A pagination loop or an unbounded retry is where a count grows without anybody deciding it should.