twittrapi – Twitter/X API client for Python
Official Python client for TwittrAPI, a REST API for public Twitter/X data: tweets, profiles, followers, search, lists and communities. From $0.065 per 1,000 calls, no X developer account.
Get an API key: twittrapi.com/start · Pricing · Docs · Status · also available on RapidAPI
SDKs and integrations: Node.js · Go · MCP server · n8n · Make · Apify · LangChain · LlamaIndex · Agent skill · Examples
- Sync (
TwittrAPI) and async (AsyncTwittrAPI) clients with the same methods - Works with a direct TwittrAPI key or a RapidAPI key
- Retries 429, 5xx and network errors with backoff
- Typed exceptions for every error class
- A cursor-following paginator
- Python 3.9+, one dependency (
httpx), fully type-annotated
Responses are X's raw GraphQL JSON, returned as plain dict/list values. The client does not
reshape them.
Not affiliated with X Corp.
Install
pip install twittrapi
Quickstart
Direct API key
Get a key at twittrapi.com.
from twittrapi import TwittrAPI
client = TwittrAPI(api_key="YOUR_KEY")
user = client.users.get_by_username("jack")
print(user)
Requests go to https://api.twittrapi.com with an X-API-KEY header.
RapidAPI key
from twittrapi import TwittrAPI
client = TwittrAPI(rapidapi_key="YOUR_RAPIDAPI_KEY")
user = client.users.get_by_username("jack")
Requests go to https://twitter-aio.p.rapidapi.com with x-rapidapi-key and x-rapidapi-host
headers.
Environment variables
With no key argument, the client reads TWITTRAPI_API_KEY, then RAPIDAPI_KEY:
export TWITTRAPI_API_KEY=YOUR_KEY
client = TwittrAPI()
TWITTRAPI_BASE_URL overrides the direct API base URL (useful for a proxy or staging).
Client options
client = TwittrAPI(
api_key="YOUR_KEY",
base_url="https://api.twittrapi.com", # default depends on the auth mode
timeout=60.0, # seconds, or an httpx.Timeout
max_retries=2, # retries for 429, 5xx and network errors
response_format="raw", # or "simple", see "Simple format" below
)
You can also pass your own httpx.Client (or httpx.AsyncClient) as http_client= to set
proxies, transports or connection limits. The client does not close an http_client you pass in.
Close the client when done, or use it as a context manager:
with TwittrAPI() as client:
...
Examples by resource
IDs can be passed as str or int. Usernames can include a leading @.
Users
client.users.get_by_username("jack")
client.users.get_by_id("12")
client.users.username_to_id("jack") # smaller response when you only need the ID
client.users.get_many(["12", "44196397"]) # up to 100 IDs per call
# Timelines take user_id or username=
client.users.tweets("12", count=20)
client.users.tweets(username="jack")
client.users.tweets_and_replies("12")
client.users.media("12")
client.users.followers("12", count=100)
client.users.following(username="jack")
client.users.verified_followers("12")
Passing user_id is a little faster than username, because the API does not have to look the
handle up first.
Tweets
client.tweets.get("1585841080431321088") # tweet plus reply thread
client.tweets.get("1585841080431321088", ranking_mode="Recency") # Relevance, Recency or Likes
client.tweets.retweeters("1585841080431321088")
client.tweets.likers("1585841080431321088") # often empty: X hides likers for many tweets
client.tweets.quotes("1585841080431321088")
Search
client.search.tweets("python")
client.search.tweets('from:jack "hello" lang:en', category="Latest", count=20)
query supports X search operators (from:, to:, lang:, since:, until:, min_faves:,
filter:media, -filter:replies, ...). category is one of Top (default), Latest,
People, Photos, Videos.
Lists
client.lists.details("1234567890")
client.lists.tweets("1234567890")
client.lists.members("1234567890")
Communities
client.communities.search("python")
client.communities.get("1500000000000000000")
client.communities.tweets("1500000000000000000")
Trends
client.trends.get(1) # worldwide
client.trends.get(23424977) # United States
client.trends.locations() # every location with trends, with its WOEID
Simple format
By default every method returns X's raw JSON. The API can also answer in a compact, flat,
camelCase format with a fixed schema (docs). It is
opt-in: set it for the whole client, or per call with response_format= on any method.
client = TwittrAPI(response_format="simple")
page = client.users.tweets(username="XDevelopers", count=20)
# {"data": [{"id": ..., "text": ..., "author": {...}, "counts": {...}, ...}],
# "nextCursor": "...", "previousCursor": "...", "hasNextPage": true}
raw = client.users.tweets(username="XDevelopers", response_format="raw") # this call only
twittrapi.models has typed dataclasses for it (Tweet, User, Page, ...), with
snake_case attributes:
from twittrapi.models import User, tweets_page, users_page
page = tweets_page(client.users.tweets(username="XDevelopers"))
for tweet in page.data:
print(tweet.id, tweet.counts.likes, tweet.author.username if tweet.author else None)
print(page.next_cursor, page.has_next_page)
user = User.from_dict(client.users.get_by_username("XDevelopers")["data"])
Same price, parameters and errors as the raw format; only successful responses change. Account and status routes ignore the option and answer as usual.
Pagination
Paginated methods accept count (1 to 100; leave it out for the server default of 20) and
cursor. count is only a hint to X: a page can hold more or fewer results. X puts the next-page cursor inside the response
(a timeline entry with cursorType: "Bottom"). client.paginate() finds it and keeps calling
the method for you, yielding each raw page:
for page in client.paginate(client.users.followers, username="jack", count=100, max_pages=5):
... # page is the raw JSON for one request
Paging stops when any of these happens:
max_pagespages were fetched (default 10,Nonefor no limit)- no bottom cursor is found in the page
- the cursor repeats
- the page has no results, only cursor entries
Each page is a separate billed request, so set max_pages to what you need.
To start from a saved cursor, pass cursor=.... If you want to page by hand, use
extract_bottom_cursor:
from twittrapi import extract_bottom_cursor
page = client.search.tweets("python", category="Latest")
next_cursor = extract_bottom_cursor(page)
if next_cursor:
page = client.search.tweets("python", category="Latest", cursor=next_cursor)
Cursor detection is best-effort. X does not document its response format, and the helper
searches the whole response for the shapes X uses today (cursorType: "Bottom" objects and
cursor-bottom-* entry IDs). If X changes the format, the paginator may stop early.
In the simple format none of this guessing is needed: every list response carries
nextCursor and hasNextPage, and paginate() follows nextCursor until hasNextPage is
false. That applies when the client was created with response_format="simple" or when you
pass response_format="simple" to paginate():
for page in client.paginate(client.users.followers, username="jack", response_format="simple"):
for user in page["data"]:
print(user["username"])
Error handling
All errors raised by the client inherit from TwittrAPIError.
| Exception | When |
|---|---|
ValidationError |
400, 422: bad parameters |
AuthenticationError |
401, 403: missing or invalid key, or no access |
InsufficientBalanceError |
402: no balance left on the account |
NotFoundError |
404: user, tweet, list or community not found |
RateLimitError |
429: too many requests; has retry_after (seconds or None) |
ServerError |
5xx |
APIStatusError |
any other 4xx; base class of all the above |
APIConnectionError |
network failure, no response |
APITimeoutError |
request timed out (subclass of APIConnectionError) |
HTTP errors carry status_code, message (the API's error text), body (parsed JSON) and
response (the httpx.Response).
from twittrapi import (
TwittrAPI,
InsufficientBalanceError,
NotFoundError,
RateLimitError,
TwittrAPIError,
)
client = TwittrAPI()
try:
user = client.users.get_by_username("someone")
except NotFoundError:
print("No such user")
except InsufficientBalanceError:
print("Top up at https://twittrapi.com")
except RateLimitError as exc:
print(f"Rate limited, retry in {exc.retry_after}s")
except TwittrAPIError as exc:
print(f"API error: {exc}")
Invalid arguments (a non-numeric ID, more than 100 IDs, an unknown category) raise
ValueError or TypeError before any request is sent.
Retries
429 responses, 5xx responses and network errors are retried up to max_retries times (default 2,
so at most 3 attempts). The wait is 0.5 s, 1 s, 2 s, ... with some jitter, capped at 8 s. For 429
the client waits for the Retry-After header if the API sends one (capped at 60 s). Other 4xx
errors, including 402, are never retried. Set max_retries=0 to turn retries off.
Async
AsyncTwittrAPI has the same constructor and methods. Every API method is a coroutine, and
paginate returns an async iterator.
import asyncio
from twittrapi import AsyncTwittrAPI
async def main() -> None:
async with AsyncTwittrAPI() as client:
user, trends = await asyncio.gather(
client.users.get_by_username("jack"),
client.trends.get(1),
)
async for page in client.paginate(client.search.tweets, "python", max_pages=3):
...
asyncio.run(main())
More examples
The examples/ folder has short scripts:
export_followers_csv.py: export a user's followers to CSVkeyword_monitor.py: poll search and print new matching tweetsuser_timeline_json.py: save a user's tweets to JSONtrends_by_country.py: trends for a country or city by name
Development
python -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/pytest
.venv/bin/mypy
Support
Questions, bugs or a higher rate limit: email support@twittrapi.com or open an issue in this repository. Service status: twittrapi.com/status. News and changes: the TwittrAPI Telegram channel (link on the website).
License
MIT. See LICENSE.
TwittrAPI is an independent service and is not affiliated with, endorsed by or sponsored by X Corp. "X" and "Twitter" are trademarks of X Corp.
Metadata
Release files for twittrapi 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| twittrapi-0.2.0.tar.gz | 55.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| twittrapi-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 84.7 kB
Release files / twittrapi-0.2.0.tar.gz
| Download URL | twittrapi-0.2.0.tar.gz |
|---|---|
| Size | 55.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c05237776e1c9074ed124f76dc9916de9988e3882f14ff0b5e455f7ef5d29492
|
|
BLAKE2b-256 checksum How to use checksums |
0e7bd48ecb244e337abd2febdbd7c6b24e9c0e4f766e4ea9a666d696870c3bb7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency logRelease files / twittrapi-0.2.0-py3-none-any.whl
| Download URL | twittrapi-0.2.0-py3-none-any.whl |
|---|---|
| Size | 29.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e1b8e4fd67d94382b0d8043d8b4dbe2e11ed365cb0e06124897a6a5e29b74ca7
|
|
BLAKE2b-256 checksum How to use checksums |
24a67f616db450b1a4ed6c310f957bdbbda82ed7e74b3a8df62b8d1957490f80
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency log