Metadata-Version: 2.5
Name: tweetapi
Version: 2.5.0
Summary: Python SDK for TweetAPI's Twitter/X data and account API
Project-URL: Homepage, https://tweetapi.com?utm_source=pypi&utm_medium=readme&utm_campaign=python-sdk
Project-URL: Documentation, https://tweetapi.com/docs?utm_source=pypi&utm_medium=readme&utm_campaign=python-sdk
Project-URL: Repository, https://github.com/tweetapi/python
Project-URL: Issues, https://github.com/tweetapi/python/issues
Author-email: TweetAPI <support@tweetapi.com>
License-Expression: MIT
Keywords: social media api,tweet api,tweetapi,tweets,twitter,twitter api,twitter client,twitter data,twitter library,twitter sdk,twitter wrapper,x api
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: requests>=2.28.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: responses>=0.23.0; extra == 'dev'
Description-Content-Type: text/markdown

# TweetAPI Python SDK

`tweetapi` is the Python SDK for [TweetAPI](https://tweetapi.com?utm_source=github&utm_medium=readme&utm_campaign=python-sdk). It provides methods for users, tweets, posts, profiles, interactions, lists, communities, Spaces, search, authentication, and direct messages.

## Install

```bash
pip install tweetapi
```

## Quick start

```python
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](https://tweetapi.com?utm_source=github&utm_medium=readme&utm_campaign=python-sdk) 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 `proxy` or 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 `RateLimitError` and `NotFoundError`.
- 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`.

```python
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:

```python
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:

```python
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:

```python
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`:

```python
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:

```python
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:

```python
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:

```python
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_after` seconds, up to `max_retry_delay`. That is the API's `Retry-After`, or 60 seconds if it sends none.
- For a 5xx response, it waits for `Retry-After` when 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`.

```python
# 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:

```python
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:

```python
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 code
- `status_code`: HTTP status code
- `message`: human-readable error message
- `details`: response context such as a field, reason, or retry delay

### Error codes

`ErrorCode` provides the codes as constants. Network failures use `"CONNECTION_ERROR"`.

```python
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

```python
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+
- `requests` library

## Links

- [Documentation](https://tweetapi.com/docs?utm_source=github&utm_medium=readme&utm_campaign=python-sdk)
- [Create an API key](https://tweetapi.com?utm_source=github&utm_medium=readme&utm_campaign=python-sdk)
- [Dashboard](https://tweetapi.com/dashboard?utm_source=github&utm_medium=readme&utm_campaign=python-sdk)
- [Node.js SDK](https://github.com/tweetapi/node)

## License

MIT

TweetAPI is a third-party service and is not affiliated with X Corp.
