WEIR

How to Get Tweets with Python in 2026: The Official API, Scrapers, and a REST Option

How to get tweets with Python in 2026: what the official X API costs, why scraping libraries keep breaking, and working code for search, paging and CSV.

By Tarun Khatri · · 8 min read

There are three ways to get tweets with Python in 2026: the official X API (through Tweepy or X's own SDK), which bills $0.005 per post read; an unofficial scraping library such as twscrape, which needs logged-in X accounts and breaks when X changes its site; or a third-party REST API that you call with requests. This guide covers what each one costs and how fragile it is, then walks through the third route end to end: searching posts, reading an account's timeline, paging with a cursor, and saving everything to CSV.

Disclosure: Weir, the REST API used in the main example, is our product. The official route is covered too, with its real prices, and it is the right choice in some cases that are listed below.

TL;DR

Route What you need Cost to read posts (as of October 2026) Main risk
Official X API (Tweepy, XDK) X developer account, prepaid credits $0.005 per post read; 3M post reads/month cap Cost at volume
Scraping libraries (twscrape, twikit) Your own X accounts and cookies Free in money Breaks without warning; X's terms
Weir REST API An API key 1 credit per X post plus 1 per call; 5,000 free credits a month A third party, not X itself

How to get tweets with Python: the three routes

1. The official X API with Tweepy

X moved its API to pay-per-usage pricing: you buy credits, and each request deducts from them. As of October 2026 the rate card lists Posts: Read at $0.005 per resource and User: Read at $0.010 per resource. The same page says pay-per-usage plans are capped at 3 million post reads per billing cycle, and that the same resource requested twice within one UTC day is charged once. New accounts get $20 in credits after saving a card and making a first purchase.

Search comes in two flavours (X docs): recent search (/2/tweets/search/recent) covers the last 7 days and returns up to 100 posts per request; full-archive search (/2/tweets/search/all) goes back to 2006, returns up to 500 per request, and is available to pay-per-use and Enterprise customers.

Tweepy is the long-standing community Python client for it, and it is still maintained (version 4.17.0, released July 2026). A minimal recent search:

import os
import tweepy

client = tweepy.Client(bearer_token=os.environ["X_BEARER_TOKEN"])

response = client.search_recent_tweets(
    query="python lang:en -is:retweet",
    max_results=100,
    tweet_fields=["created_at", "public_metrics", "author_id"],
)

for tweet in response.data or []:
    print(tweet.id, tweet.created_at, tweet.public_metrics["like_count"])

X also publishes an official Python SDK, xdk, if you would rather not depend on a community library.

The arithmetic: 100 posts per request at $0.005 each is $0.50 a request, so 10,000 posts cost $50 before any deduplication. The upside is that this is X's own interface, under X's own developer terms, with write access and a 500-per-request archive search.

Note that the v2 API has its own query syntax. min_faves: and min_retweets: are rejected with a 400; v2 uses min_likes:, min_reposts:, is:reply, has:media and so on. Our X advanced search operators reference maps the two dialects.

2. Scraping libraries: twscrape, twikit, snscrape

These libraries call the same internal endpoints x.com's own web app uses. Their status, checked on GitHub and PyPI on 19 October 2026:

  • snscrape: the last release on PyPI is from June 2023, and GitHub issue #996, "All Twitter scrapes are failing: blocked (404)", opened on 30 June 2023, is still open. Treat its Twitter module as dead.
  • twikit: the last PyPI release, 2.3.3, is from February 2025. The repository's recent issues include "KeyError since April 23 2026" and a pull request to fix a SearchTimeline 404. It may work on a given day, but it depends on fixes that have not shipped as a release.
  • twscrape: actively maintained (0.20.1, August 2026). Its README is upfront that it "requires authorized X/Twitter accounts", added from browser cookies, and notes that X's Terms of Service discourage using multiple accounts.

All three share the same structural problem: when X changes an internal endpoint or tightens login checks, they break until a maintainer catches up, and your accounts carry the risk. X's Terms of Service (effective 9 October 2026) say you "cannot scrape the Services without X's express written permission". This is not legal advice, but it is a consideration you should weigh before building on a scraper.

3. A REST API over plain HTTP

The third route is a hosted API that returns X data as JSON. Weir is one; there are others, and our comparison of Twitter API alternatives covers them fairly. The rest of this guide uses Weir because every endpoint and field below is checked against its OpenAPI spec.

Step by step: get tweets with Python and requests

Step 1: set up

Create a key under API keys after signing up (no card), then:

pip install requests
export WEIR_API_KEY="weir_live_..."

One shared session keeps the auth header in one place:

import os
import requests

API = "https://api.weirapp.io"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['WEIR_API_KEY']}"

Step 2: search posts

Search is POST /v1/x/search with a JSON body. You can write X's search syntax directly in query, or use structured fields such as fromUsers, language, minLikes, onlyOriginal, since and until, which Weir turns into the operators for you:

response = session.post(
    f"{API}/v1/x/search",
    json={
        "fromUsers": ["XDevelopers"],
        "minLikes": 500,
        "onlyOriginal": True,  # exclude replies
        "since": "2026-08-01T00:00:00Z",
        "sort": "latest",
        "limit": 20,
    },
    timeout=60,
)
response.raise_for_status()
page = response.json()

for post in page["data"]:
    print(post["createdAt"], post["author"]["username"], post["likeCount"], post["url"])

print("credits charged:", response.headers["x-credits-charged"])

Each item uses X's own field names. A trimmed result from a real run of that request:

{
  "id": "2102535041532186709",
  "text": "X Livestream API has been rebuilt from the ground up. ...",
  "url": "https://x.com/XDevelopers/status/2102535041532186709",
  "lang": "en",
  "createdAt": "2026-09-22T23:06:31.000Z",
  "author": {
    "username": "XDevelopers",
    "name": "Developers",
    "verified": true,
    "followersCount": 698077
  },
  "likeCount": 564,
  "retweetCount": 76,
  "replyCount": 89,
  "viewCount": 433017
}

Two details to know. id is a string because X ids exceed what a float can hold exactly; keep it a string. And counts can be null, which means X did not report the number, not zero.

Step 3: get a user's posts

An account's timeline is GET /v1/x/users/{user}/posts. {user} takes a username (with or without @) or a numeric id, and limit goes up to 20:

response = session.get(
    f"{API}/v1/x/users/XDevelopers/posts",
    params={"limit": 20},
    timeout=60,
)
response.raise_for_status()
timeline = response.json()

It returns the account's own posts and reposts, newest first, with any pinned post first. Replies live at /v1/x/users/{user}/replies. A protected or missing account is a 404.

Step 4: paginate with the cursor

Every list response carries meta.nextCursor. Send it back unchanged, with the same body, to get the next page; null is the only stop condition. For search the cursor goes in the JSON body; for GET endpoints it goes in the query string.

import time

def search_pages(body, max_pages=10):
    """Yield posts from POST /v1/x/search, page by page."""
    cursor, pages = None, 0
    while pages < max_pages:
        payload = {**body, **({"cursor": cursor} if cursor else {})}
        response = session.post(f"{API}/v1/x/search", json=payload, timeout=60)

        if response.status_code == 429:  # too fast: wait as told, then retry
            time.sleep(int(response.headers.get("Retry-After", "5")))
            continue
        response.raise_for_status()  # a 402 means out of credits: stop, don't retry

        page = response.json()
        yield from page["data"]
        pages += 1

        cursor = page["meta"]["nextCursor"]
        if not cursor:
            return

Three things this loop gets right:

  • A page ceiling. max_pages keeps a broad query from paging, and billing, forever.
  • 429 and 402 are treated differently. A 429 clears after Retry-After seconds. A 402 means the account is out of credits and no amount of retrying fixes it (errors).
  • The body never changes between pages. A cursor is bound to the request that produced it; change a filter and you get a 400, not a silently different result set.

X serves roughly 20 posts per search page however high you set limit, so plan on pages of about 20.

Step 5: save to CSV

import csv

FIELDS = ["id", "createdAt", "username", "text", "likeCount",
          "retweetCount", "replyCount", "viewCount", "url"]

query = {"query": "python", "language": "en", "minLikes": 50, "onlyOriginal": True}

with open("posts.csv", "w", newline="", encoding="utf-8") as file:
    writer = csv.DictWriter(file, fieldnames=FIELDS)
    writer.writeheader()
    for post in search_pages(query, max_pages=5):
        writer.writerow({
            "id": post["id"],
            "createdAt": post["createdAt"],
            "username": post["author"]["username"],
            "text": post["text"],
            "likeCount": post["likeCount"],
            "retweetCount": post["retweetCount"],
            "replyCount": post["replyCount"],
            "viewCount": post["viewCount"],
            "url": post["url"],
        })

encoding="utf-8" matters: posts are full of emoji and non-Latin scripts. If you open the file in Excel, ids will display in scientific notation; that is Excel's display, and the CSV itself keeps every digit.

What that script costs

An X post is 1 credit and every successful call costs 1 credit on top, so a 20-post page is 21 credits (billing). Five pages is about 105 credits. The 5,000 free monthly credits cover roughly 238 such pages, or about 4,760 posts. On the smallest paid pack ($5 for 25,000 credits, which never expire), 10,000 posts is 500 pages times 21 credits, or 10,500 credits: about $2.10. The same 10,000 reads on the official API's rate card is $50. Full numbers are on the pricing page.

The free tier allows 30 requests per minute, and a search counts as 2, so a tight loop gets about 15 search pages a minute before it sees a 429.

Which route should you use?

Use the official X API when you need to post or act on behalf of users, need an agreement with X itself, need full-archive search at 500 posts per request, or your organisation requires first-party data sources. It costs more per read, but nothing else gives you the same standing.

Avoid building on scraping libraries for anything that has to keep running. They are useful for one-off exploration, but they put your own accounts at risk and break on X's schedule rather than yours.

Use a read-only REST API when you only need to read public posts, profiles, replies or Lists, and want plain HTTP with predictable per-result pricing. Be aware it is a third party: it is not X, and it cannot give you rights X has not granted. For the wider market, see X API pricing in 2026.

Doing this with Weir

If the third route fits, create a free account and run the code above; 5,000 credits arrive every month with no card. The quickstart gets you to a first request, the searchX reference lists every search field, and pagination covers cursors in more depth. Reddit works the same way: see how to get Reddit data with Python.

Sources