TweetAPI Python SDK
tweetapi is the Python SDK for TweetAPI. It provides methods for users, tweets, posts, profiles, interactions, lists, communities, Spaces, search, authentication, and direct messages.
Install
pip install tweetapi
Quick start
from tweetapi import TweetAPI
client = TweetAPI(api_key="YOUR_API_KEY")
# Get a user profile
user = client.user.get_by_username(username="elonmusk")
print(user["data"]["followerCount"])
# Search tweets
results = client.explore.search(query="bitcoin", type="Latest")
# Get followers with pagination
followers = client.user.get_followers(user_id="123456")
next_page = client.user.get_followers(
user_id="123456",
cursor=followers["pagination"]["nextCursor"],
)
Create an API key with 100 included requests. No credit card is required.
SDK behavior
- Public methods have type annotations, and response models use
TypedDict. - Reads retry rate limits, server errors, timeouts, and connection failures by default. Writes retry only when the API confirms the request never reached X, so an action is never sent twice. Username changes and accepting or denying follow requests disable automatic retries.
- Errors that a retry cannot fix fail at once: a failing
proxyor a used-up or expired plan. paginate()yields items from cursor-based responses;paginate_pages()yields full response pages.- HTTP errors map to exception classes such as
RateLimitErrorandNotFoundError. - Timeouts can use one value or separate connect and read values.
- The package supports Python 3.9+ and depends on
requests.
API reference
User
| Method | Description |
|---|---|
client.user.get_by_username(username=...) |
Get user profile by username |
client.user.get_by_usernames(usernames=...) |
Get multiple users (comma-separated) |
client.user.get_by_user_id(user_id=...) |
Get user profile by ID |
client.user.get_by_user_ids(user_ids=...) |
Get multiple users by IDs |
client.user.get_tweets(user_id=...) |
Get a user's tweets |
client.user.get_tweets_and_replies(user_id=...) |
Get tweets and replies |
client.user.get_following(user_id=...) |
Get who a user follows |
client.user.get_followers(user_id=...) |
Get a user's followers |
client.user.get_verified_followers(user_id=...) |
Get verified followers |
client.user.get_subscriptions(user_id=...) |
Get subscriptions |
client.user.get_following_v1(user_id=...) |
Get following (v1, supports count) |
client.user.get_followers_v1(user_id=...) |
Get followers (v1, supports count) |
client.user.get_following_ids(user_id=...) |
Get following user IDs |
client.user.get_followers_ids(user_id=...) |
Get follower user IDs |
client.user.check_follow(subject_id=..., target_id=...) |
Check follow relationship |
client.user.about_account(username=...) |
Get account transparency info |
Tweet
| Method | Description |
|---|---|
client.tweet.get_details_and_conversation(tweet_id=...) |
Get tweet details and replies |
client.tweet.get_details_by_ids(ids=...) |
Get multiple tweets (max 200) |
client.tweet.get_retweets(tweet_id=...) |
Get users who retweeted |
client.tweet.get_quotes(tweet_id=...) |
Get quote tweets |
client.tweet.translate(tweet_id=..., dst_lang=...) |
Translate a tweet |
Post
| Method | Description |
|---|---|
client.post.create_post(auth_token=..., text=..., proxy=..., reply_option=...) |
Create a tweet |
client.post.create_post_quote(auth_token=..., text=..., attachment_url=..., proxy=..., reply_option=...) |
Quote tweet |
client.post.create_post_with_media(auth_token=..., text=..., media=..., proxy=..., reply_option=...) |
Tweet with media |
client.post.reply_post(auth_token=..., text=..., tweet_id=..., proxy=...) |
Reply to a tweet |
client.post.reply_post_with_media(...) |
Reply with media |
client.post.delete_post(auth_token=..., tweet_id=...) |
Delete a tweet |
Profile
| Method | Description |
|---|---|
client.profile.update(auth_token=..., name=..., bio=..., location=..., website=..., proxy=...) |
Update profile fields |
client.profile.avatar(auth_token=..., media=..., proxy=...) |
Update profile avatar |
client.profile.banner(auth_token=..., media=..., proxy=...) |
Update profile banner |
client.profile.remove_banner(auth_token=..., proxy=...) |
Remove the profile banner |
client.profile.set_privacy(auth_token=..., is_private=..., proxy=...) |
Make the account public or private |
client.profile.update_username(auth_token=..., password=..., username=..., proxy=...) |
Update the account username |
Interaction
| Method | Description |
|---|---|
client.interaction.favorite_post(auth_token=..., tweet_id=...) |
Like a tweet |
client.interaction.unfavorite_post(auth_token=..., tweet_id=...) |
Unlike a tweet |
client.interaction.retweet(auth_token=..., tweet_id=...) |
Retweet |
client.interaction.delete_retweet(auth_token=..., tweet_id=...) |
Remove retweet |
client.interaction.bookmark(auth_token=..., tweet_id=...) |
Bookmark a tweet |
client.interaction.delete_bookmark(auth_token=..., tweet_id=...) |
Remove bookmark |
client.interaction.follow(auth_token=..., user_id=...) |
Follow a user |
client.interaction.unfollow(auth_token=..., user_id=...) |
Unfollow a user |
client.interaction.get_follow_requests(auth_token=..., proxy=..., cursor=..., count=...) |
List pending follow requester IDs |
client.interaction.accept_follow_request(auth_token=..., user_id=..., proxy=...) |
Accept one pending follow request |
client.interaction.deny_follow_request(auth_token=..., user_id=..., proxy=...) |
Deny one pending follow request |
client.interaction.add_member_to_list(auth_token=..., list_id=..., user_id=...) |
Add to list |
client.interaction.remove_member_from_list(auth_token=..., list_id=..., user_id=...) |
Remove from list |
client.interaction.get_notifications(auth_token=...) |
Get notifications |
client.interaction.get_user_analytics(auth_token=...) |
Get analytics |
Review follow requests
The supplied auth_token determines whose pending requests you can review. Listing returns data as a list of digit strings and pagination with nextCursor and prevCursor. IDs retain their full precision; the response includes no profiles or total count. An empty list is valid, and terminal cursors are None.
requests_page = client.interaction.get_follow_requests(auth_token="AUTH_TOKEN")
print(requests_page["data"])
next_cursor = requests_page["pagination"]["nextCursor"]
if next_cursor is not None:
next_page = client.interaction.get_follow_requests(
auth_token="AUTH_TOKEN", cursor=next_cursor, count=100,
)
Omit cursor and count (or pass None) to use the server defaults of "-1" and 100. When supplied, count must be an integer from 1 to 100. Each method accepts an optional proxy in host:port@user:pass format.
After reviewing the IDs, select one request and choose an action:
selected_user_id = "9007199254740993123" # A requester ID you reviewed
decision = "accept" # Choose "accept" or "deny"
if decision == "accept":
result = client.interaction.accept_follow_request(
auth_token="AUTH_TOKEN", user_id=selected_user_id,
)
elif decision == "deny":
result = client.interaction.deny_follow_request(
auth_token="AUTH_TOKEN", user_id=selected_user_id,
)
Pass user_id as a digit-only string, not a username. Successful actions return data.id, data.action ("accept_follow_request" or "deny_follow_request"), data.timestamp, data.success, and data.metadata.user_id.
These actions are not guaranteed to be idempotent and are never retried automatically. A timeout leaves the outcome uncertain: check the pending requests and account state before deciding whether to retry manually. Listing keeps the client's normal retry behavior and works with paginate() and paginate_pages().
List
| Method | Description |
|---|---|
client.list.get_details(list_id=...) |
Get list details |
client.list.get_tweets(list_id=...) |
Get tweets in a list |
client.list.get_members(list_id=...) |
Get list members |
client.list.get_followers(list_id=...) |
Get list followers |
client.list.create(auth_token=..., name=..., description=..., is_private=...) |
Create a list |
client.list.add_member(auth_token=..., list_id=..., user_id=..., proxy=...) |
Add a member |
client.list.remove_member(auth_token=..., list_id=..., user_id=..., proxy=...) |
Remove a member |
Community
| Method | Description |
|---|---|
client.community.get_details(community_id=...) |
Get community details |
client.community.get_tweets(community_id=..., sort_by=...) |
Get community tweets |
client.community.get_members(community_id=...) |
Get members |
client.community.search(query=...) |
Search communities |
client.community.create_post(auth_token=..., text=..., community_id=..., proxy=...) |
Post in community |
client.community.create_post_with_media(...) |
Post with media |
client.community.create_quote(auth_token=..., text=..., attachment_url=..., community_id=..., proxy=...) |
Quote post in community |
client.community.create_quote_with_media(...) |
Quote post with media |
client.community.reply_post(...) |
Reply to community post |
client.community.reply_post_with_media(...) |
Reply with media |
client.community.join(auth_token=..., community_id=...) |
Join a community |
client.community.leave(auth_token=..., community_id=...) |
Leave a community |
Space
| Method | Description |
|---|---|
client.space.get_by_id(space_id=...) |
Get Space details |
client.space.get_stream_url(media_key=...) |
Get HLS stream URL |
Explore
| Method | Description |
|---|---|
client.explore.search(query=..., type=...) |
Search tweets, users, photos, or videos |
Auth
| Method | Description |
|---|---|
client.auth.login(username=..., password=..., proxy=..., country=...) |
Log in and return auth tokens |
country is the ISO 3166-1 alpha-2 code for the proxy's public egress IP (for example, "US"). It must match the IP used for the complete login attempt. Pass two_factor_secret when the account uses TOTP-based 2FA.
X Chat (encrypted DMs)
| Method | Description |
|---|---|
client.xchat.setup(auth_token=..., user_id=..., pin=...) |
Initialize encrypted DMs |
client.xchat.get_conversations(auth_token=...) |
List conversations |
client.xchat.send(auth_token=..., recipient_id=..., message=...) |
Send message |
client.xchat.get_history(auth_token=..., conversation_id=...) |
Get conversation history |
client.xchat.can_dm(auth_token=..., user_ids=...) |
Check DM availability |
Unencrypted DMs
| Method | Description |
|---|---|
client.dm.send_dm(auth_token=..., conversation_id=..., text=..., proxy=...) |
Send DM |
client.dm.get_dm_permissions(auth_token=..., recipient_ids=...) |
Check permissions |
client.dm.get_inbox_initial_state(auth_token=...) |
Get inbox state |
client.dm.get_inbox_trusted(auth_token=..., cursor=...) |
Get the trusted inbox |
client.dm.get_inbox_untrusted(auth_token=..., cursor=...) |
Get message requests |
client.dm.get_conversation(auth_token=..., conversation_id=...) |
Get messages |
client.dm.get_dm_user_updates(auth_token=..., cursor=...) |
Get DM user updates |
client.dm.accept_conversation(auth_token=..., conversation_id=...) |
Accept a conversation request |
Posting and profile media
Tweet media accepts an existing TweetAPI media ID, a URL, or inline base64 data:
client.post.create_post_with_media(
auth_token="AUTH_TOKEN",
text="Launch update",
media=[{"media_id": "1971008286821380096"}],
proxy="host:port:user:pass",
)
Use reply_option to set reply controls when creating standard, quote, or media posts:
client.post.create_post(
auth_token="AUTH_TOKEN",
text="Followers can reply",
proxy="host:port:user:pass",
reply_option={"mode": "followers"},
)
Profile avatar and banner uploads accept a URL or inline data plus type:
client.profile.update(auth_token="AUTH_TOKEN", name="New Name", bio="Builder")
client.profile.avatar(
auth_token="AUTH_TOKEN",
media={"url": "https://example.com/avatar.jpg"},
)
client.profile.banner(
auth_token="AUTH_TOKEN",
media={"data": "BASE64_IMAGE_DATA", "type": "image/png"},
)
client.profile.remove_banner(auth_token="AUTH_TOKEN")
client.profile.set_privacy(auth_token="AUTH_TOKEN", is_private=True)
client.profile.update_username(
auth_token="AUTH_TOKEN",
password="ACCOUNT_PASSWORD",
username="NEW_USERNAME",
)
Use client.list for list mutations. The older helpers under client.interaction remain available:
created = client.list.create(
auth_token="AUTH_TOKEN",
name="Research",
description="Accounts to watch",
is_private=True,
)
client.list.add_member(auth_token="AUTH_TOKEN", list_id=created["data"]["id"], user_id="123")
client.list.remove_member(auth_token="AUTH_TOKEN", list_id=created["data"]["id"], user_id="123")
Community quote posts support the same URL and media attachment shapes:
client.community.create_quote(
auth_token="AUTH_TOKEN",
text="Useful context",
attachment_url="https://x.com/user/status/123",
community_id="987",
proxy="host:port:user:pass",
)
client.community.create_quote_with_media(
auth_token="AUTH_TOKEN",
text="Useful context",
attachment_url="https://x.com/user/status/123",
community_id="987",
media=[{"media_id": "1971008286821380096"}],
proxy="host:port:user:pass",
)
Pagination
paginate() yields individual items. paginate_pages() yields each full response page:
from tweetapi import TweetAPI, paginate, paginate_pages
client = TweetAPI(api_key="YOUR_API_KEY")
# Iterate individual items across all pages
for user in paginate(
lambda cursor: client.user.get_followers(user_id="123456", cursor=cursor),
):
print(user["username"])
# Iterate full pages (access page-level data)
for page in paginate_pages(
lambda cursor: client.explore.search(query="bitcoin", type="Latest", cursor=cursor),
max_pages=5, # optional: limit number of pages
):
print(f"Got {len(page['data'])} results")
print(f"Next cursor: {page['pagination']['nextCursor']}")
Each helper accepts a callable that takes a cursor and returns a response with data and pagination.nextCursor. Use max_pages to limit the number of fetched pages.
Retries
The client retries reads after these transient failures. Reads are every GET request, plus tweet.translate() and the X Chat get_conversations(), get_history(), and can_dm() calls.
- For a 429 response, it waits for
RateLimitError.retry_afterseconds, up tomax_retry_delay. That is the API'sRetry-After, or 60 seconds if it sends none. - For a 5xx response, it waits for
Retry-Afterwhen the API sends one. - Otherwise, and for a timeout or connection failure, it uses exponential backoff with up to 25% jitter.
By default, the client makes up to 3 retries. Its base delay starts at 1 second, doubles after each attempt, and is capped at 30 seconds. Other 4xx responses are not retried.
Writes, such as posts, replies, likes, follows, messages, and logins, retry only after a 429 or 503 response that includes Retry-After. The API sends that header only before a request reaches X, so the retry cannot repeat an action. Any other failure of a write, including a timeout, may have happened after X accepted the action. The client raises the error instead, so you can check the result before trying again.
These errors are never retried, because repeating the request cannot succeed:
code |
Status | Cause |
|---|---|---|
PROXY_ERROR |
502 | Your proxy refused the connection or could not be reached |
PROXY_TIMEOUT |
504 | Your proxy timed out |
QUOTA_EXHAUSTED |
429 | Your plan allowance or prepaid balance is used up |
SUBSCRIPTION_INACTIVE |
429 | Your plan expired or is inactive |
Username changes and accepting or denying follow requests make one attempt regardless of max_retries.
# Customize retry behavior
client = TweetAPI(
api_key="YOUR_API_KEY",
max_retries=5, # default: 3
initial_retry_delay=2.0, # default: 1.0 (seconds)
backoff_multiplier=3.0, # default: 2.0
max_retry_delay=60.0, # default: 30.0 (seconds)
)
# Disable retries entirely
client = TweetAPI(api_key="YOUR_API_KEY", max_retries=0)
Rate-limit state
After a 429 response, client.rate_limit_info records the retry delay and the time the response was received:
print(client.rate_limit_info)
Error handling
The client raises retryable errors after it exhausts the configured retries. It raises other errors from the first response:
from tweetapi import (
TweetAPI,
TweetAPIError,
AuthenticationError,
RateLimitError,
NotFoundError,
ValidationError,
ServerError,
NetworkError,
)
client = TweetAPI(api_key="YOUR_API_KEY")
try:
user = client.user.get_by_username(username="elonmusk")
except RateLimitError as e:
print(f"Rate limited. Retry in {e.retry_after}s")
except NotFoundError:
print("User not found")
except AuthenticationError:
print("Invalid API key")
except ValidationError as e:
print(f"Bad request: {e.message}")
except ServerError:
print("API is having issues, try again later")
except NetworkError:
print("Network error — check your connection")
except TweetAPIError as e:
print(f"Error [{e.code}]: {e.message}")
Every error includes these attributes:
code: API error code, such as"ACCOUNT_LOCKED"or"RATE_LIMITED", or"UNKNOWN_ERROR"when the response has no specific codestatus_code: HTTP status codemessage: human-readable error messagedetails: response context such as a field, reason, or retry delay
Error codes
ErrorCode provides the codes as constants. Network failures use "CONNECTION_ERROR".
from tweetapi import ErrorCode, TweetAPIError
try:
client.post.create_post(auth_token=auth_token, text="Hello", proxy=proxy)
except TweetAPIError as e:
if e.code == ErrorCode.ACCOUNT_LOCKED:
print("Unlock the account on X, then try again")
code |
Status | Meaning |
|---|---|---|
RATE_LIMITED |
429 | Per-minute limit reached. retry_after gives the seconds until it resets. During login, X can also return it without Retry-After. |
QUOTA_EXHAUSTED |
429 | Plan allowance or prepaid balance used up |
SUBSCRIPTION_INACTIVE |
429 | Plan expired or inactive |
ACCOUNT_SUSPENDED |
403 | The account behind auth_token is suspended |
ACCOUNT_LOCKED |
403 | The account behind auth_token is locked |
PROXY_ERROR |
502 | Your proxy refused the connection or could not be reached |
PROXY_TIMEOUT |
504 | Your proxy timed out |
INVALID_CREDENTIALS |
401 | Login: wrong username or password |
TWO_FACTOR_REQUIRED |
401 | Login: the account needs two_factor_secret |
INVALID_TWO_FACTOR_CODE |
401 | Login: the two-factor code was rejected |
EMAIL_VERIFICATION_REQUIRED |
401 | Login: the account must verify its email first |
LOGIN_RUNTIME_UNAVAILABLE |
503 | Login: X could not be reached; try again later |
Configuration
client = TweetAPI(
api_key="YOUR_API_KEY", # Required
base_url="https://...", # Optional (default: https://api.tweetapi.com)
timeout=30, # Optional; one value for connect and read
connect_timeout=10.0, # Optional (default: 10s)
read_timeout=30.0, # Optional (default: 30s)
max_retries=3, # Optional (default: 3, set 0 to disable)
backoff_multiplier=2.0, # Optional (default: 2.0)
initial_retry_delay=1.0, # Optional (default: 1.0s)
max_retry_delay=30.0, # Optional (default: 30.0s)
)
# You can also pass a tuple for timeout
client = TweetAPI(api_key="YOUR_API_KEY", timeout=(5, 30)) # (connect, read)
Requirements
- Python 3.9+
requestslibrary
Links
License
MIT
TweetAPI is a third-party service and is not affiliated with X Corp.
Metadata
Release files for tweetapi 2.5.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 | |
|---|---|---|---|
| tweetapi-2.5.0.tar.gz | 19.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tweetapi-2.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 46.9 kB
Release files / tweetapi-2.5.0.tar.gz
| Download URL | tweetapi-2.5.0.tar.gz |
|---|---|
| Size | 19.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6321acfac663bcce248ae8c12cddc7bd60ff9bd9efeeb23f72ba4de6ce82c8b2
|
|
BLAKE2b-256 checksum How to use checksums |
144f772746bb3d71feda1389c358689268f7f4ad8ece6d555d68f34f4297c6a1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / tweetapi-2.5.0-py3-none-any.whl
| Download URL | tweetapi-2.5.0-py3-none-any.whl |
|---|---|
| Size | 27.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
55acae6539b88610de30b7d5d37669025aab6893221915151bf564d22b9128f8
|
|
BLAKE2b-256 checksum How to use checksums |
3b7bc03e3fda7797fc0e5840cec71bec8b89b34c36f41e615ffc62a3bd15b1fe
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|