# snscrape and twscrape alternative in 2026

> snscrape no longer works for X and twscrape needs your own X accounts. Swap the scraper for two small Python functions over an API and keep the rest of your code.

HTML page: https://www.twittrapi.com/blog/snscrape-twscrape-alternative
Published 2026-10-30.

If your snscrape or twscrape script stopped returning tweets, use TwittrAPI: it returns the same public data over plain HTTPS with one API key. You need no X developer account, no X accounts and no cookies. This post shows each old call next to its replacement, so you can swap the scraper and keep the rest of your code.

## Why the old scrapers stopped working

**snscrape.** It read X without logging in. When X put a login wall in front of logged-out visitors in mid-2023, its Twitter module stopped working. The issue "All Twitter scrapes are failing" was opened on June 30, 2023 and is still open. Other snscrape modules are not affected.

**twscrape.** It still works, because it logs in with X accounts you supply (a login or the `auth_token` and `ct0` cookies). That is also where the trouble is: limits apply per account, accounts get locked or rate limited, and you have to keep adding new ones. The scraping runs under those accounts, and X's terms apply to them.

**Nitter.** Most public Nitter instances have shut down, so RSS feeds and HTML parsing built on them no longer return data.

[How to scrape X data in 2026](https://www.twittrapi.com/blog/how-to-scrape-twitter-x-data) covers the rules and what still works without an API.

## Setup

Create a key on [/start](https://www.twittrapi.com/start). New accounts get $0.50 of test credit for 7 days, about 5,000 calls. Put the key in an environment variable:

```bash
export TWITTRAPI_API_KEY="your-key"
pip install requests
```

Every request sends the key in the `X-API-KEY` header. With `format=simple` you get flat JSON with fixed field names ([simple format](https://www.twittrapi.com/docs/simple-format)).

## Search tweets by query

The search query syntax is X's own, so your existing query strings work unchanged: `from:`, `since:`, `until:`, `lang:`, `min_faves:`, `-filter:replies` and the rest.

**Before (snscrape):**

```python
import itertools

import snscrape.modules.twitter as sntwitter

query = "python lang:en since:2026-10-01"
for tweet in itertools.islice(sntwitter.TwitterSearchScraper(query).get_items(), 100):
    print(tweet.date, tweet.user.username, tweet.rawContent)
```

**Before (twscrape):**

```python
import asyncio

from twscrape import API, gather

async def main():
    api = API()  # accounts added earlier with `twscrape add_accounts` and `twscrape login_accounts`
    tweets = await gather(api.search("python lang:en since:2026-10-01", limit=100))
    for tweet in tweets:
        print(tweet.date, tweet.user.username, tweet.rawContent)

asyncio.run(main())
```

**After (TwittrAPI):**

```python
import os

import requests

API = "https://api.twittrapi.com"
HEADERS = {"X-API-KEY": os.environ["TWITTRAPI_API_KEY"]}

def pages(path, params, max_pages=50):
    """Yield pages from a paginated endpoint, following the cursor."""
    params = {**params, "format": "simple"}
    for _ in range(max_pages):
        r = requests.get(f"{API}{path}", headers=HEADERS, params=params, timeout=30)
        r.raise_for_status()
        page = r.json()
        yield page
        if not page["hasNextPage"]:
            return
        params["cursor"] = page["nextCursor"]

def search(query, limit=100):
    """Tweets matching an X search query, newest first."""
    n = 0
    for page in pages("/search", {"searchItem": query, "category": "Latest", "count": 100}):
        for t in page["data"]:
            if t.get("unavailable"):
                continue
            yield t
            n += 1
            if n >= limit:
                return

for t in search("python lang:en since:2026-10-01", limit=100):
    print(t["createdAt"], t["author"]["username"], t["text"])
```

The endpoint is [`GET /search`](https://www.twittrapi.com/docs/reference/search). `category=Latest` returns every match newest first, which is what snscrape's search scraper gave you. Leave it out to get X's `Top` tab. `max_pages` is a safety limit: every page is a billed call.

## Get a user's tweets

**Before (snscrape):**

```python
for tweet in itertools.islice(sntwitter.TwitterUserScraper("NASA").get_items(), 100):
    print(tweet.date, tweet.rawContent)
```

**Before (twscrape):**

```python
# inside main(), as above
user = await api.user_by_login("NASA")
tweets = await gather(api.user_tweets(user.id, limit=100))
```

**After (TwittrAPI):**

```python
def user_tweets(username, limit=100):
    """Tweets from a profile, newest first, without replies."""
    n = 0
    for page in pages("/user/-1/tweets", {"username": username, "count": 100}):
        for t in page["data"]:
            if t.get("unavailable"):
                continue
            yield t
            n += 1
            if n >= limit:
                return

for t in user_tweets("NASA", limit=100):
    print(t["createdAt"], t["text"])
```

The endpoint is [`GET /user/{userId}/tweets`](https://www.twittrapi.com/docs/reference/user-userid-tweets). Passing `-1` as the user ID and the handle as `username` saves the separate lookup that twscrape needs. If you already store numeric IDs, put the ID in the path instead.

How far back a profile goes is up to X: timelines stop after a few thousand tweets, for scrapers and for us alike. For older tweets, search with `from:NASA until:2020-01-01` and walk back by date. [Get tweets from an account with Python](https://www.twittrapi.com/blog/get-tweets-from-twitter-account-python) adds CSV export and a daily job that only fetches new tweets.

## Pagination

Every list endpoint works the same way: a page holds `data`, `hasNextPage` and `nextCursor`. Send `nextCursor` back as `cursor` until `hasNextPage` is `false`. The `pages` helper above does exactly that.

Figure: paging through tweets with a cursor. Call /search, pass the returned cursor back as `cursor` for the next page, and stop at the last page. Every page is one call ($0.065 to $0.10 per 1,000 calls).

`count` is a hint; X decides how many tweets a page really holds. A page usually holds about 20 tweets, so 1,000 tweets cost about $0.00325 to $0.005, depending on your tier. The [pricing page](https://www.twittrapi.com/pricing) has the rates per 1,000 calls.

## Field names, old and new

snscrape and twscrape use mostly the same attribute names. Here is where each one lives in the simple format:

| snscrape / twscrape | TwittrAPI (simple format) |
| --- | --- |
| `tweet.id` | `t["id"]` (a string; compare as `int`) |
| `tweet.url` | `t["url"]` |
| `tweet.rawContent` | `t["text"]` |
| `tweet.date` | `t["createdAt"]` (ISO 8601 string, UTC) |
| `tweet.lang` | `t["lang"]` |
| `tweet.user.username` | `t["author"]["username"]` |
| `tweet.user.displayname` | `t["author"]["name"]` |
| `tweet.likeCount` | `t["counts"]["likes"]` |
| `tweet.retweetCount` | `t["counts"]["retweets"]` |
| `tweet.replyCount` | `t["counts"]["replies"]` |
| `tweet.quoteCount` | `t["counts"]["quotes"]` |
| `tweet.viewCount` | `t["counts"]["views"]` |
| `tweet.quotedTweet` | `t["quoted"]` |
| `tweet.retweetedTweet` | `t["retweetOf"]` |

If the rest of your code expects a `datetime`, convert once:

```python
from datetime import datetime

created = datetime.fromisoformat(t["createdAt"].replace("Z", "+00:00"))
```

The [field table](https://www.twittrapi.com/docs/simple-format#tweet) lists every field, including `media`, `urls`, `hashtags`, `mentions` and `replyTo`.

## Other calls you may have used

| What you did before | TwittrAPI endpoint |
| --- | --- |
| Profile by handle (`user_by_login`, `TwitterUserScraper(...).entity`) | [`GET /user/by/username/{username}`](https://www.twittrapi.com/docs/reference/user-by-username-username) |
| Tweets and replies | [`GET /user/{userId}/tweetsAndReplies`](https://www.twittrapi.com/docs/reference/user-userid-tweetsandreplies) |
| Media tweets | [`GET /user/{userId}/media`](https://www.twittrapi.com/docs/reference/user-userid-media) |
| Followers / following | [`GET /user/{userId}/followers`](https://www.twittrapi.com/docs/reference/user-userid-followers), [`GET /user/{userId}/followings`](https://www.twittrapi.com/docs/reference/user-userid-followings) |
| One tweet by ID | [`GET /tweet/{tweetId}`](https://www.twittrapi.com/docs/reference/tweet-tweetid) |
| Replies to a tweet | [`GET /tweet/{tweetId}/replies`](https://www.twittrapi.com/docs/reference/tweet-tweetid-replies) |
| List tweets | [`GET /lists/tweets`](https://www.twittrapi.com/docs/reference/lists-tweets) |

All of them take the same key, `format=simple` and `cursor`. The [API reference](https://www.twittrapi.com/docs) has the parameters.

## Prefer an SDK?

The [`twittrapi` package](https://www.twittrapi.com/docs/integrations#sdks) wraps the same calls, retries 429 and 5xx responses, and follows the cursor for you:

```python
from twittrapi import TwittrAPI

client = TwittrAPI(response_format="simple")  # key from TWITTRAPI_API_KEY

for page in client.paginate(client.search.tweets, "python lang:en", category="Latest", count=100, max_pages=10):
    for t in page["data"]:
        print(t["createdAt"], t["author"]["username"], t["text"][:80])
```

Searching without code first? [Search Twitter without a developer account](https://www.twittrapi.com/blog/search-twitter-without-developer-account) covers operators, filters and polling.

## Frequently asked questions

### Does snscrape still work for Twitter in 2026?

No. Its Twitter module stopped working when X required login in mid-2023, and the issue reporting that all Twitter scrapes fail is still open. Use an API such as TwittrAPI to get the same public tweets.

### Is twscrape still working?

It works as long as you supply logged-in X accounts and keep them healthy. Accounts can get locked or rate limited, and the tool can break when X changes its internal endpoints. TwittrAPI needs no X accounts or cookies.

### Can I keep my snscrape search queries?

Yes. TwittrAPI's search takes X's search syntax, so queries with from:, since:, until:, lang: and similar operators work unchanged. Use category=Latest for the newest-first order snscrape gave you.

### How much does it cost to replace a scraper?

You pay per call, and a search or timeline call returns a page of tweets. At about 20 tweets per page, 1,000 tweets cost $0.00325 to $0.005. New accounts get $0.50 of test credit.

## Sources

- [snscrape issue #996: All Twitter scrapes are failing](https://github.com/JustAnotherArchivist/snscrape/issues/996)
- [twscrape on GitHub](https://github.com/vladkens/twscrape)
- [Search reference](https://www.twittrapi.com/docs/reference/search), [user tweets reference](https://www.twittrapi.com/docs/reference/user-userid-tweets) and [simple format](https://www.twittrapi.com/docs/simple-format) (TwittrAPI docs)

TwittrAPI is not affiliated with X Corp. It reads only public data.
