LANGUAGE CLIENTS
TwitterAPIs language clients
Is there a TwitterAPIs SDK, or do I write my own client?
TwitterAPIs is a plain REST API, so the client is whatever HTTP library your language already has. There is no package to install and no SDK version to keep current. All 109 endpoints are plain HTTPS with one Authorization header and ordinary query parameters, so a complete client is about fifteen lines. TwitterAPIs ships all 6 of them here, per our own SDK count: Python, Node.js, Go, PHP, Ruby, and curl.
Python
Full walkthrough and error handling
Node.js
Full walkthrough and error handling
Go
Full walkthrough and error handling
PHP
Full walkthrough and error handling
Each client below is complete and runnable. It reads the key from an environment variable, sets the Bearer header, builds the query string, checks the status code, and decodes the JSON. Change the endpoint path and the parameters and the same function covers every read endpoint the API serves.
Twitter API in Python
Install nothing but an HTTP library (pip install requests), set one header, and every read endpoint is one function call away.
import os
import requests
BASE = "https://api.twitterapis.com"
KEY = os.environ["TWITTERAPIS_KEY"]
session = requests.Session()
session.headers["Authorization"] = f"Bearer {KEY}"
def get(path, **params):
"""Call any read endpoint. path is the docs path, e.g. 'user/info'."""
res = session.get(f"{BASE}/twitter/{path}", params=params, timeout=30)
res.raise_for_status()
return res.json()
# Resolve a handle to the permanent numeric user ID
profile = get("user/info", username="naval")
print(profile["user"]["id"], profile["user"]["followers_count"])
# Search posts, then page with the returned cursor
page = get("tweet/advanced_search", query="from:naval min_faves:500")
for tweet in page["tweets"]:
print(tweet["id"], tweet["text"][:80])Full Python client for the Twitter API walks through pagination, rate limits and error handling.
Twitter API in Node.js
Install nothing but an HTTP library (No dependency. fetch is built in from Node 18.), set one header, and every read endpoint is one function call away.
const BASE = "https://api.twitterapis.com";
const KEY = process.env.TWITTERAPIS_KEY;
async function get(path, params = {}) {
const url = new URL(`/twitter/${path}`, BASE);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${KEY}` },
});
if (!res.ok) {
throw new Error(`${res.status} ${await res.text()}`);
}
return res.json();
}
// Resolve a handle to the permanent numeric user ID
const { user } = await get("user/info", { username: "naval" });
console.log(user.id, user.followers_count);
// Search posts
const page = await get("tweet/advanced_search", {
query: "from:naval min_faves:500",
});
for (const tweet of page.tweets) console.log(tweet.id, tweet.text);Full Node.js client for the Twitter API walks through pagination, rate limits and error handling.
Twitter API in Go
Install nothing but an HTTP library (No dependency. net/http and encoding/json are in the standard library.), set one header, and every read endpoint is one function call away.
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
)
const base = "https://api.twitterapis.com"
func get(path string, params url.Values, out any) error {
req, err := http.NewRequest("GET", base+"/twitter/"+path+"?"+params.Encode(), nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("TWITTERAPIS_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return fmt.Errorf("twitterapis: status %d", res.StatusCode)
}
return json.NewDecoder(res.Body).Decode(out)
}
type profile struct {
User struct {
ID string `json:"id"`
FollowersCount int `json:"followers_count"`
} `json:"user"`
}
func main() {
var p profile
if err := get("user/info", url.Values{"username": {"naval"}}, &p); err != nil {
panic(err)
}
fmt.Println(p.User.ID, p.User.FollowersCount)
}Full Go client for the Twitter API walks through pagination, rate limits and error handling.
Twitter API in PHP
Install nothing but an HTTP library (No dependency. curl ships with most PHP builds.), set one header, and every read endpoint is one function call away.
<?php
const BASE = "https://api.twitterapis.com";
function twitterapis_get(string $path, array $params = []): array {
$url = BASE . "/twitter/" . $path . "?" . http_build_query($params);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("TWITTERAPIS_KEY")],
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException("twitterapis: status $status");
}
return json_decode($body, true);
}
$profile = twitterapis_get("user/info", ["username" => "naval"]);
echo $profile["user"]["id"], " ", $profile["user"]["followers_count"], PHP_EOL;Full PHP client for the Twitter API walks through pagination, rate limits and error handling.
Twitter API in Ruby
Install nothing but an HTTP library (No dependency. net/http and json are in the standard library.), set one header, and every read endpoint is one function call away.
require "net/http"
require "json"
require "uri"
BASE = "https://api.twitterapis.com"
def get(path, params = {})
uri = URI("#{BASE}/twitter/#{path}")
uri.query = URI.encode_www_form(params)
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{ENV.fetch('TWITTERAPIS_KEY')}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req)
end
raise "twitterapis: status #{res.code}" unless res.code == "200"
JSON.parse(res.body)
end
profile = get("user/info", username: "naval")
puts profile["user"]["id"], profile["user"]["followers_count"]Twitter API in cURL
Install nothing but an HTTP library (No dependency.), set one header, and every read endpoint is one function call away.
curl "https://api.twitterapis.com/twitter/user/info?username=naval" \
-H "Authorization: Bearer $TWITTERAPIS_KEY"
curl -G "https://api.twitterapis.com/twitter/tweet/advanced_search" \
--data-urlencode "query=from:naval min_faves:500" \
-H "Authorization: Bearer $TWITTERAPIS_KEY"Why no package to install
An SDK earns its keep when the protocol underneath it is hard: OAuth dances, token refresh, streaming connections, pagination with inconsistent shapes, retry semantics that differ per endpoint. None of that is true here. Authentication is one static header. Paging is one cursor field you pass back. Every response is JSON with the payload under a named key. A wrapper over that adds a dependency you have to keep current and a layer you have to debug through, and it buys you almost nothing.
The one case where generated code does pay is a large integration in a strongly typed language. The public OpenAPI 3.1 specification covers every endpoint, so a generator will give you typed models for all of them. Both routes are supported and neither is privileged: what we commit to is that the specification and the server agree.
Python quickstart
Three lines of setup and one call. Install nothing but an HTTP library, put the key in the environment rather than the source, and point a single helper at any of the 65 read endpoints. The helper below is the whole client: it sets the Bearer header once on a session, joins the docs path onto the base URL, raises on a non-2xx so a failure is loud rather than an empty result, and returns decoded JSON.
import os
import requests
BASE = "https://api.twitterapis.com"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['TWITTERAPIS_KEY']}"
def get(path, **params):
res = session.get(f"{BASE}/twitter/{path}", params=params, timeout=30)
res.raise_for_status()
return res.json()
profile = get("user/info", username="naval")
print(profile["user"]["id"], profile["user"]["followers_count"])Two things are worth knowing before you build on it. Read the key from TWITTERAPIS_KEY rather than pasting it, because a key in source is a key in your git history. And set an explicit timeout: the default in most HTTP clients is no timeout at all, which turns one slow upstream call into a hung job.
Search posts, and page through them
Search is tweet/advanced_search and it takes the same operator syntax you would type into the site: from:, min_faves:, since: and the rest compose in one query string. Results come back under tweets, and a page carries a next_cursor you pass straight back as cursor to get the next one.
# BOTH terminators, because the two endpoint families fail in
# opposite directions: search nulls the cursor on a page that is still FULL,
# and follower-graph endpoints never null it at all. Either guard alone loops.
MAX_CALLS = 200
cursor, calls, seen = None, 0, set()
while calls < MAX_CALLS:
page = get("tweet/advanced_search",
query="from:naval min_faves:500",
**({"cursor": cursor} if cursor else {}))
calls += 1
tweets = page.get("tweets") or []
if not tweets:
break # empty page: nothing left
for tweet in tweets:
print(tweet["id"], tweet["text"][:80])
cursor = page.get("next_cursor")
if not cursor:
break # cursor gone: this WAS the last page
if cursor in seen: # catches a stuck cursor AND any cycle
raise RuntimeError(f"cursor stopped advancing at {cursor}")
seen.add(cursor)
else:
print(f"stopped at the {MAX_CALLS}-call cap, resume from {cursor}")Billing is per CALL, not per tweet returned, so a narrow query that fills a page is the cheapest shape and a broad query you filter client-side is the most expensive. Push the filter into the operator string wherever you can. A page is up to about 20 tweets, which is a ceiling and not a promise: a query with few matches returns a short page and bills exactly what a full one bills.
Follower export files
Exporting a follower graph is the one paging loop with a trap in it, and it is ours to warn you about because it is a property of the upstream graph rather than of your code. On list and affiliate endpoints next_cursor comes back null on the final page, so the obvious loop until the cursor is null terminates correctly. Follower-graph endpoints do not: they keep returning a NON-NULL cursor after the last real page. A loop written against the cursor alone runs forever and bills every lap.
Stop when the collection array comes back EMPTY, not when the cursor goes null. Write each page out as you receive it rather than accumulating in memory, so a large account does not need a large process and an interrupted export leaves you a partial file you can resume from instead of nothing.
import json
# BOTH terminators, plus a repeat check. Follower-graph endpoints return a
# NON-NULL cursor even on the last page, so the empty collection is what ends
# THIS call; but user/followers_v2 nulls the cursor on a page that is still
# full, and omitting the cursor restarts at page one, so the cursor guard has
# to be here too. A cursor that never changes is the third way this loops.
MAX_CALLS = 500
cursor, calls, seen = None, 0, set()
with open("followers.ndjson", "w") as out:
while calls < MAX_CALLS:
page = get("user/followers",
username="naval",
**({"cursor": cursor} if cursor else {}))
calls += 1
users = page.get("users") or []
if not users: # <- the real terminator
break
for user in users:
out.write(json.dumps(user) + "\n")
out.flush() # partial file stays valid and resumable
cursor = page.get("next_cursor")
if not cursor:
break # cursor spent: that WAS the last page
if cursor in seen: # catches a stuck cursor AND any cycle
raise RuntimeError(f"cursor stopped advancing at {cursor}")
seen.add(cursor)
else:
print(f"stopped at the {MAX_CALLS}-call cap, resume from {cursor}")NDJSON rather than one JSON array is deliberate: one record per line means the file is valid after every write, streams into jq, DuckDB, BigQuery and pandas without a parse step, and survives being cut off mid-export. Keep the cursor beside the output if you want a resumable job; it is an opaque string and the only thing you need to continue.
What a call actually returns
Every client above pages until it runs out of results, so the number that decides your bill is how many records a call returns on average, not the page size you asked for. We measured ours rather than estimating it: 396,817 successful tweet-returning read calls from the biller's own usage logs, 13 to 17 August 2026.
| Call | Records per call | Cost per 1,000 records |
|---|---|---|
user/tweets (timeline) | 18.78 | $0.043 |
bulk reads, excluding tweet/detail | 12.96 | $0.062 |
tweet/advanced_search | 7.62 | $0.105 |
The headline $0.04 per 1,000 assumes a FULL page of 20. Timeline reads land close to it. Search does not, and the reason is worth designing around: 29.5% of search calls in that window returned ZERO records and still billed, because billing is per CALL. A search-heavy job should be budgeted from 7.62, not from 20.
Sizing a job before you run it
With per-call billing the estimate is one division, and doing it before the loop runs is the difference between a job you can predict and one you discover on an invoice. Divide the records you need by the measured yield for the endpoint you are actually calling, then multiply the calls by $0.0008.
# Measured records per call (biller usage_logs, 396,817 calls, Aug 2026)
YIELD = {"user/tweets": 18.78, "bulk": 12.96, "tweet/advanced_search": 7.62}
PRICE_PER_CALL = 0.0008
def estimate(records_needed, endpoint="tweet/advanced_search"):
calls = records_needed / YIELD[endpoint]
return calls, calls * PRICE_PER_CALL
calls, usd = estimate(50_000, "tweet/advanced_search")
print(f"{calls:,.0f} calls, about ${usd:,.2f}") # 6,562 calls, about $5.25
# Cap by CALLS, not by records: a query that returns nothing still bills.
MAX_CALLS = 7000Two habits fall out of it. Cap the loop by CALLS rather than by records, because a query returning nothing still consumes the call and an uncapped loop over a sparse query is the one way to spend more than you planned. The $0.50 of credit a new account starts with is about 625 calls at this rate, which is enough to run the estimate above against real data before you commit to a budget. And prefer the narrowest endpoint that answers the question: a timeline read yields roughly two and a half times what a search read does for the same money, so pushing a filter into the operator string instead of filtering client-side changes the bill rather than just the code.
Before you write the client
Get a key first, then confirm it works with a single call. The quickstart takes about a minute end to end, and the API key guide covers where the key comes from. Once a call succeeds, the REST API reference covers the paging model and the error contract, and the ID finder is a fast way to sanity-check a handle before you wire it into a job. For agent workflows, the MCP server is a published npm package and is the one case where you do install something.
Frequently Asked Questions
No, and that is deliberate rather than a gap. Every endpoint is one HTTPS GET or POST with a single Authorization header and ordinary query parameters, so a wrapper would add an install step, a version to keep current, and a layer to debug through, in exchange for saving about eight lines. The clients on this page are those eight lines, written properly, for you to paste into your own codebase and own outright.
Not directly. Those libraries are built against the official X API surface, its endpoints, its auth model and its response shapes, so they cannot be pointed at a different provider by changing a base URL. Replacing one is usually less work than it sounds: the calls you actually use are typically a handful, and each becomes a single function like the ones on this page.
You can, and for a large integration in a typed language it is often the better answer. The specification is public and complete, so a generator gives you typed request and response models for every endpoint without hand-writing them. For a handful of calls the plain client on this page is simpler to read and simpler to debug, which is why it is the default suggestion here.
Send the header Authorization: Bearer YOUR_API_KEY on every request. The API also accepts x-api-key with the same value if that fits your stack better. There is no OAuth handshake, no token refresh, no app registration and no callback URL, because you are calling us rather than calling X, and we hold the X side ourselves.
JSON, with the payload under a named key rather than at the top level. A profile lookup returns an object with a user key, a search returns a tweets array plus a cursor for the next page. The full field list for every endpoint is in the reference documentation and in the public OpenAPI 3.1 specification, which you can also feed to a generator if you would rather have typed models than raw dictionaries.
There is one ceiling to design around, 600 requests a minute and 20 concurrent per key, so the retry logic you need is the ordinary kind: honour Retry-After on a 429, retry a 5xx with backoff, do not retry any other 4xx, and set a timeout on every call. Cost is per call at $0.0008 for a standard read, so the thing worth engineering is not throttling but deduplication, making sure a retry loop cannot silently bill you twice for a result you already hold.