# TwitterAPIs SDK and Language Clients Canonical: https://www.twitterapis.com/sdk Description: There is no SDK to install. Copy a working TwitterAPIs client for Python, Node, Go, PHP, Ruby or curl, add one header, and start reading X data. Generated: 2026-09-14T03:00:37.964Z --- 1. [Home](/) 2. / Language clients 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 ](/sdk/python)[ Node.js Full walkthrough and error handling ](/sdk/node)[ Go Full walkthrough and error handling ](/sdk/go)[ PHP Full walkthrough and error handling ](/sdk/php) 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. twitterapis.py Copy ``` 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](/sdk/python) 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. twitterapis.mjs Copy ``` 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](/sdk/node) 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. twitterapis.go Copy ``` 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](/sdk/go) 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. twitterapis.php Copy ``` 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](/sdk/php) 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. twitterapis.rb Copy ``` 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. lookup.sh Copy ``` 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. Python Copy ``` 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. Python Copy ``` # 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. Python Copy ``` 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. Python Copy ``` # 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 = 7000 ``` Two 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](/quickstart) takes about a minute end to end, and the [API key guide](/twitter-api-key) covers where the key comes from. Once a call succeeds, the [REST API reference](/twitter-rest-api) covers the paging model and the error contract, and [the ID finder](/tools/twitter-id-finder) is a fast way to sanity-check a handle before you wire it into a job. For agent workflows, the [MCP server](/mcp) is a published npm package and is the one case where you do install something. ## Frequently Asked Questions ### Is there an official TwitterAPIs SDK? 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. ### Can I use Tweepy or another Twitter library with this API? 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. ### Should I generate a client from the OpenAPI spec? 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. ### How do I authenticate from my language? 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. ### What does a response look like? 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. ### How do I handle rate limits and retries? 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. [View API Docs](https://docs.twitterapis.com)[Start Free](/signup?utm_source=aio&utm_medium=organic&utm_campaign=aeo-sdk) [ TwitterAPIs ](/) The cheapest pay-as-you-go Twitter and X API. $0.0008 per call, which works out to $0.04 per 1,000 tweets on a full 20-tweet page. No subscriptions and no developer account. ## Product / API - [Pricing](/pricing) - [Cost Calculator](/twitter-api-cost-calculator) - [Rate Limits](/twitter-api-rate-limits) - [MCP Server](/mcp) - [Integrations](/integrations) - [Language Clients](/sdk) - [Changelog](/changelog) - [Status](/status) ## Developers - [Documentation](https://docs.twitterapis.com) - [API Reference](https://docs.twitterapis.com/docs/reference/search/tweet-advanced-search) - [User Info](https://docs.twitterapis.com/docs/reference/user-reads/user-info) - [User Tweets](https://docs.twitterapis.com/docs/reference/user-reads/user-tweets) - [Advanced Search](https://docs.twitterapis.com/docs/reference/search/tweet-advanced-search) - [Verified Followers](https://docs.twitterapis.com/docs/reference/follower-graph/user-verified-followers) ## Resources / Compare - [Answers](/answers) - [Reviews](/reviews) - [Free Tools](/tools) - [Twitter ID Finder](/tools/twitter-id-finder) - [Get a Twitter API Key](/twitter-api-key) - [Official X API Comparison](/twitter-api-pricing) - [Twitter API Use Cases](/twitter-api-usecases) - [Twitter API Alternatives](/twitter-api-alternatives) - [Twitter Unofficial API](/twitter-unofficial-api) - [Twitter Free API](/twitter-free-api) - [TwitterAPIs vs twitterapi.io](/twitterapis-vs-twitterapi-io) - [TwitterAPIs vs GetXAPI](/twitterapis-vs-getxapi) - [TwitterAPIs vs TweetAPI](/twitterapis-vs-tweetapi) - [TwitterAPIs vs TwexAPI](/twitterapis-vs-twexapi) - [TwitterAPIs vs RapidAPI](/twitterapis-vs-rapidapi) ## Legal - [About](/about) - [Security](/security) - [Trust](/privacy-and-data-handling) - [Terms of Service](/terms-of-service) - [Affiliates](/affiliates) - [Contact](/contact) - [Jobs](/jobs) © 2026 TwitterAPIs. All rights reserved. TwitterAPIs is an independent third-party API for developers and researchers. Not affiliated with, endorsed by, or sponsored by X Corp. All systems operational