Skip to main content

twittrapi-python: Python client for Twitter/X data, sync and async, no X developer account

twittrapi – Twitter/X API client for Python

PyPI Python Tests License: MIT

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")
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")
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_pages pages were fetched (default 10, None for 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 CSV
  • keyword_monitor.py: poll search and print new matching tweets
  • user_timeline_json.py: save a user's tweets to JSON
  • trends_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)

Source distribution for twittrapi 0.2.0
File Size Uploaded
twittrapi-0.2.0.tar.gz 55.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for twittrapi 0.2.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page