Skip to content

Twitter API in Python: a 2026 tutorial with working code

To get Twitter/X data in Python, call an X data API with requests or a client such as Tweepy, send your key with every request and follow the cursor page by page; the official X API bills $0.005 per post.

Short answer: Tweepy and X's SDK, xdk, are free libraries for the official X API, which needs a developer account, a bearer token and prepaid credits: $0.005 per post, $0.015 with its author from November 22, 2026. To read public data, use Sorsa API with requests: one key, 20 tweets per request from $0.07 per 1,000; 100 free requests cover 2,000.

Endpoint
POST /search-tweets
Page size
20 tweets per request
Price
from $0.07 per 1,000 tweets

#Does Tweepy still work in 2026?

Yes. Tweepy 4.17.0 shipped on July 2, 2026, and X now publishes its own Python SDK, xdk, at version 0.10.6. Both are clients of the official X API, so every call they make needs a developer account, an app's bearer token and prepaid credits.

The credits are what older tutorials leave out. New developers get no free tier, and X bills each object a response returns: $0.005 per post and $0.010 per user, up to 3M post reads a month (X's price table (opens in a new tab)). From November 22, 2026 the authors and other objects pulled in through expansions are billed at the same rates (X's notice of October 23 (opens in a new tab)), so the usual Tweepy search pays for every author it attaches:

Python (Tweepy)
import tweepy

client = tweepy.Client(bearer_token="YOUR_BEARER_TOKEN")
resp = client.search_recent_tweets(
    "python lang:en",
    max_results=100,
    expansions=["author_id"],
)
# 100 posts: $0.50. From Nov 22, 2026, with authors: up to $1.50

The library itself is free and open source. Its homepage still says the API behind it has free and paid tiers, which no longer holds for a new account. Every rate, cap and date is laid out in X API pricing.

What the official route needs before the first call

  • A developer account at console.x.com, opened with an X account and X's Developer Agreement accepted.
  • An app, whose bearer token X shows only once (X's access guide (opens in a new tab)).
  • Credits bought in advance and spent per post and per user returned.
  • OAuth user tokens for anything that posts, likes or follows.

Reading public posts and profiles needs none of this. A Twitter/X data API with its own key answers over plain HTTPS, and requests is the only client a script needs.

#Get tweets in Python with one API key

To read X data from Python, use Sorsa API, a Twitter (X) API and scraper: one ApiKey header instead of OAuth, 20 requests per second on every plan, free accounts included, and search results from $0.07 per 1,000 tweets. It returns public posts, profiles and followers as JSON with the author's full profile inside every tweet. It never asks for your X login, so your X account carries no risk.

Set up the key and send the first request

  1. Get a key. Sign up and copy your API key from the dashboard. The first 100 requests are free, need no card and never expire.

  2. Install requests and store the key. Keep the key in an environment variable rather than in the code, and call the API from a script or server, never from browser JavaScript.

    Terminal
    pip install requests
    export SORSA_API_KEY="your_key"  # PowerShell: $env:SORSA_API_KEY="your_key"
    
  3. Search for tweets. Send a query in X's search syntax to POST /search-tweets (opens in a new tab) with the key in the ApiKey header.

    import os
    import requests
    
    API = "https://api.sorsa.io/v3"
    HEADERS = {"ApiKey": os.environ.get("SORSA_API_KEY", "YOUR_API_KEY")}
    
    r = requests.post(
        f"{API}/search-tweets",
        headers=HEADERS,
        json={"query": "python lang:en", "order": "latest"},
        timeout=30,
    )
    r.raise_for_status()
    for tweet in r.json()["tweets"]:
        print(tweet["user"]["username"], tweet["full_text"][:60])
    
  4. Read the page. Each response holds up to 20 tweets and a next_cursor for the next page. The values below are demo data in the documented shape.

    Response200 OK
    {
      "tweets": [
        {
          "id": "1985432176540981234",
          "full_text": "Loaded a week of #python posts into pandas in four lines.",
          "created_at": "2026-11-01T18:42:07Z",
          "lang": "en",
          "likes_count": 42,
          "retweet_count": 7,
          "reply_count": 3,
          "quote_count": 1,
          "view_count": 3810,
          "bookmark_count": 5,
          "is_reply": false,
          "user": {
            "id": "1592041833471827968",
            "username": "py_data_notebook",
            "display_name": "Py Data Notebook",
            "followers_count": 1840,
            "verified": false
          }
        }
      ],
      "next_cursor": "DAABCgABF7Y8kQ3wAAA"
    }
    

Every tweet arrives with its counts and its author, so a script sets no expansions or field lists. These are the fields most scripts read:

FieldWhat it holdsExample
idThe tweet's 19-digit ID, sent as a string: keep it one1985432176540981234
full_textThe whole text of the postLoaded a week of #python posts
created_atWhen it was posted, ISO 8601 in UTC2026-11-01T18:42:07Z
likes_count, retweet_count, reply_count, quote_countEngagement counts42
view_count, bookmark_countViews and bookmarks3810
userThe author's profile: 19 fields such as username and followers_countpy_data_notebook, 1840
quoted_status, retweeted_statusThe quoted or reposted tweet, in the same shapeA tweet object
  • id

    What it holds
    The tweet's 19-digit ID, sent as a string: keep it one
    Example
    1985432176540981234
  • full_text

    What it holds
    The whole text of the post
    Example
    Loaded a week of #python posts
  • created_at

    What it holds
    When it was posted, ISO 8601 in UTC
    Example
    2026-11-01T18:42:07Z
  • likes_count, retweet_count, reply_count, quote_count

    What it holds
    Engagement counts
    Example
    42
  • view_count, bookmark_count

    What it holds
    Views and bookmarks
    Example
    3810
  • user

    What it holds
    The author's profile: 19 fields such as username and followers_count
    Example
    py_data_notebook, 1840
  • quoted_status, retweeted_status

    What it holds
    The quoted or reposted tweet, in the same shape
    Example
    A tweet object

#Retry 429s and server errors in one helper

Send every request through one function that retries only what is worth retrying. A 429 means more than 20 requests started within one second on your key, and a 5xx is a temporary fault on the server side. The rest of this guide builds on this call helper.

Python
import os
import time

import requests

API = "https://api.sorsa.io/v3"
RETRY = {429, 500, 502, 503, 504}

KEY = os.environ.get("SORSA_API_KEY", "YOUR_API_KEY")

session = requests.Session()
session.headers["ApiKey"] = KEY


def call(method, path, **kwargs):
    """Send one request; retry 429 and 5xx up to 4 times."""
    for attempt in range(5):
        r = session.request(method, API + path, timeout=30, **kwargs)
        if r.status_code not in RETRY or attempt == 4:
            break
        time.sleep(2 ** attempt)  # 1, 2, 4, 8 seconds
    r.raise_for_status()
    return r.json()

A 401 or 403 is never worth a retry: the key is wrong or missing, or its requests are used up, and the JSON's message field says which. Failed requests are never charged, so a retry costs nothing. Every Sorsa API endpoint only reads, so retrying a POST such as /search-tweets can't create anything twice, and since the limit resets every second, the first one-second wait clears a 429.

#Page through results with next_cursor

A cursor is an opaque key to the next page of results: send next_cursor back unchanged and stop when a response comes without one. Search and timelines return up to 20 tweets per page, and a page can hold fewer while more remain, so the item count never tells you that you reached the end.

Python
def pages(path, body, max_pages=100):
    """Yield the pages of a POST endpoint until the cursor runs out."""
    for _ in range(max_pages):
        page = call("POST", path, json=body)
        yield page
        cursor = page.get("next_cursor")
        if not cursor:
            break
        body = {**body, "next_cursor": cursor}


for page in pages("/search-tweets", {"query": "python lang:en"}):
    for tweet in page["tweets"]:
        print(tweet["id"], tweet["created_at"])

Keep the query and order unchanged while you follow one cursor, and treat max_pages as a budget, so a broad query never spends more than you planned. GET endpoints take the cursor as a query parameter instead: GET /followers (opens in a new tab), with up to 200 profiles a page, reads next_cursor from params. Two jobs have full scripts of their own: download all tweets from a user resumes a whole timeline from a saved cursor, and the guide to export Twitter followers writes follower lists to CSV.

#Fetch 100 tweets or profiles per request

When you already hold IDs or usernames, send them in batches. POST /tweet-info-bulk (opens in a new tab) takes up to 100 tweet IDs or links, and GET /info-batch (opens in a new tab) takes up to 100 usernames, each in one request.

Python
def chunks(items, size=100):
    for i in range(0, len(items), size):
        yield items[i:i + size]


tweets = []
for batch in chunks(tweet_ids):
    tweets += call("POST", "/tweet-info-bulk",
                   json={"tweet_links": batch})["tweets"]

profiles = []
for batch in chunks(usernames):
    profiles += call("GET", "/info-batch",
                     params={"usernames": batch})["users"]

Every request costs the same whatever it returns, so a batch of 100 tweets costs what one tweet costs: from $0.014 per 1,000 tweets, more than 250x less than the official X API's $5.00. With requests, a list in params goes out as a repeated parameter (usernames=a&usernames=b), the form GET /info-batch reads. Batch responses have no cursor: one response holds the whole batch, in tweets or in users.

#Save tweets to CSV or a pandas DataFrame

pandas.json_normalize turns a list of tweet objects into a DataFrame in one call, and the nested author becomes dotted columns such as user.username.

import pandas as pd

rows = []
for page in pages("/search-tweets", {"query": "python lang:en"},
                  max_pages=50):
    rows += page["tweets"]

df = pd.json_normalize(rows)
df = df[["id", "created_at", "user.username", "full_text",
         "likes_count", "retweet_count", "view_count"]]
df["created_at"] = pd.to_datetime(df["created_at"])
df.to_csv("tweets.csv", index=False)
df = pd.json_normalize(rows) one call for every tweet you collected Tweet object (JSON) { "id": "1985432176540981234", "created_at": "2026-11-01T18:42:07Z", "full_text": "Loaded a week of posts", "likes_count": 42, "user": { "username": "py_data_notebook", "followers_count": 1840 } } df.dtypes idstr created_atdatetime64, UTC full_textstr likes_countint64 user.usernamestr user.followers_countint64 user.* columns: the nested author created_at after pd.to_datetime
One call flattens each tweet into a row: the author's fields become user.* columns, and the ID stays a string.

Fifty pages make 1,000 tweets, which suits a notebook. The pandas version holds every row in memory until the end, so for jobs of a few hundred thousand tweets and more, switch to the csv version: it writes each page as it arrives and keeps memory flat however long the run.

Good to know

Tweet IDs have 19 digits and Excel keeps 15, so a CSV opened with a double click turns the last digits of every ID into zeros. Read the file back with pd.read_csv("tweets.csv", dtype={"id": str}), and in Excel import it through Data, From Text/CSV, with the ID column set to text.

#Run up to 20 requests per second with asyncio

Sorsa API allows 20 requests per second per key on every plan, and one Python process reaches that rate with asyncio once the work splits into independent chains. One cursor chain can't be split, because each page needs the cursor from the page before. So run several chains side by side, one per account or one per day of a search, under one shared pacer that keeps the key under its limit. This version needs pip install httpx.

Python
import asyncio
import os
import time
from datetime import date, timedelta

import httpx

API = "https://api.sorsa.io/v3"
HEADERS = {"ApiKey": os.environ.get("SORSA_API_KEY", "YOUR_API_KEY")}
RETRY = {429, 500, 502, 503, 504}


class Pace:
    """Start at most `rate` requests a second, evenly spaced."""

    def __init__(self, rate=20):
        self.gap, self.next = 1 / rate, 0.0
        self.lock = asyncio.Lock()

    async def wait(self):
        async with self.lock:
            now = time.monotonic()
            start = max(self.next, now)
            self.next = start + self.gap
        await asyncio.sleep(start - now)


async def fetch(client, pace, body):
    for attempt in range(5):
        await pace.wait()
        r = await client.post("/search-tweets", json=body)
        if r.status_code not in RETRY or attempt == 4:
            break
        await asyncio.sleep(2 ** attempt)
    r.raise_for_status()
    return r.json()


async def lane(client, pace, query, max_pages=100):
    """One cursor chain: its pages run one after another."""
    body, tweets = {"query": query}, []
    for _ in range(max_pages):
        page = await fetch(client, pace, body)
        tweets += page["tweets"]
        if not page.get("next_cursor"):
            break
        body = {**body, "next_cursor": page["next_cursor"]}
    return tweets


async def main(queries):
    pace = Pace(19)  # one pacer per key, one start under the limit
    async with httpx.AsyncClient(base_url=API, headers=HEADERS,
                                 timeout=30) as client:
        return await asyncio.gather(
            *(lane(client, pace, q) for q in queries))


day = date(2026, 10, 1)
days = [day + timedelta(n) for n in range(32)]  # Oct 1 to Nov 1
queries = [f"python lang:en since:{a} until:{b}"
           for a, b in zip(days, days[1:])]
results = asyncio.run(main(queries))  # Jupyter: await main(queries)
Pace(19) one pacer per key 19 starts a second, one under the limit One second on one key: limit 20, pacer 19 0 s 1 s since:2026-10-01 page 1 page 2 page 3 no cursor: done since:2026-10-02 page 1 page 2 page 3 since:2026-10-03 page 1 page 2 since:2026-10-04 page 1 page 2 page 3 page 4 Each lane follows its own next_cursor; 27 more days run as lanes of their own.
Lanes run side by side and share one pacer, so the key stays under 20 starts a second while each lane follows its own cursor.

Pace hands out start times evenly, however many lanes wait. At 19 a second it leaves one start of headroom, so network jitter can't push a second past Sorsa API's 20, and the retry catches any 429 that slips through. Keep one pacer per key: two processes with a pacer each can start twice the limit between them. At 20 requests a second, batches of 100 move up to 2,000 tweets a second and search pages 400.

Good to know

A Jupyter notebook already runs an event loop, so asyncio.run(main(queries)) stops with a RuntimeError there. In a notebook cell, write results = await main(queries) instead.

The planner turns a job into requests, time on one key and cost at Sorsa API's lowest rate.

What your script fetches
How many
Requests
5,000

20 tweets per request

Fastest on one key
about 4 min

at 20 requests a second

Sorsa API
from $7

at $1.40 per 1,000 requests

100 free requests
2,000

tweets, no card

One search is one cursor chain: split it into date windows and run them as lanes to reach this rate.

The time shown is the fastest one key allows; a single cursor chain runs slower, so split big jobs into lanes.

#What a Python job costs

Sorsa API charges per request, whatever the endpoint returns: search results cost from $0.07 per 1,000 tweets, batches from $0.014, and failed requests are never charged. The official X API charges per object instead: $5.00 per 1,000 posts, and from November 22, 2026 up to $15.00 per 1,000 when the code expands each post's author, as most Tweepy scripts do.

Tweets your script collects by search

Official X API

$5,000

for 1M tweets

$0.005 per post

Sorsa API

from$70

for 1M tweets

from $0.07 per 1,000

Authors included in every tweet

The rate per 1,000 requests runs from $4.90 on Starter to $1.40 on Hyperscale, and a plan applies it to every endpoint alike. Checking what is left costs nothing: GET /key-usage-info (opens in a new tab) never counts against your requests.

Python
usage = call("GET", "/key-usage-info")
print(usage["remaining_requests"], "left until", usage["valid_until"])

#Moving Tweepy code to Sorsa API

Most Tweepy calls map to one Sorsa API endpoint, and the code gets shorter: no expansions and no field lists, because every tweet already carries its author and counts. The table pairs each Tweepy Client method with its endpoint; to move a whole codebase, the guide to migrating from the Twitter/X API maps every v2 endpoint and response field.

Tweepy methodEndpointWhat changes
search_recent_tweetsPOST /search-tweetsAny date back to 2006, not only the last 7 days
search_all_tweetsPOST /search-tweetsThe same endpoint with since: and until: in the query
get_users_tweetsPOST /user-tweetsA username is enough, and there is no 3,200-post cap
get_tweetsPOST /tweet-info-bulk100 IDs or links per request
get_usersGET /info-batch100 usernames per request
get_users_followersGET /followers200 followers per page
get_retweetersPOST /retweeters (opens in a new tab)The same cursor loop
create_tweetnoneRead-only: post through the official X API
  • search_recent_tweets

    Endpoint
    POST /search-tweets
    What changes
    Any date back to 2006, not only the last 7 days
  • search_all_tweets

    Endpoint
    POST /search-tweets
    What changes
    The same endpoint with since: and until: in the query
  • get_users_tweets

    Endpoint
    POST /user-tweets
    What changes
    A username is enough, and there is no 3,200-post cap
  • get_tweets

    Endpoint
    POST /tweet-info-bulk
    What changes
    100 IDs or links per request
  • get_users

    Endpoint
    GET /info-batch
    What changes
    100 usernames per request
  • get_users_followers

    Endpoint
    GET /followers
    What changes
    200 followers per page
  • get_retweeters

    What changes
    The same cursor loop
  • create_tweet

    Endpoint
    none
    What changes
    Read-only: post through the official X API

Field names change as well: text becomes full_text, public_metrics["like_count"] becomes likes_count, author_id with includes["users"] becomes the user object, and next_token becomes next_cursor. Tweepy's wait_on_rate_limit=True has no counterpart to set, because X's 15-minute windows are gone from the code: the call retry and the pacer handle a limit that resets every second. POST /user-tweets (opens in a new tab) also reaches past the newest 3,200 posts, where the official timeline stops (X's timeline docs (opens in a new tab)).

#Run your first search today

Getting X data into Python takes requests, one key and four small helpers: call for retries, pages for cursors, chunks for batches of 100 and Pace for 20 requests a second. Start with Sorsa API, the Twitter API built for reading public X data at scale: 20 tweets per search request from $0.07 per 1,000, the author inside every tweet, and 100 free requests that need no card and never expire. The same calls in JavaScript are in the guide to the Twitter API in Node.js, and no-code workflows reach them through the Twitter API in n8n.

#FAQ

Is there a free Twitter API for Python?

The official X API has no free tier for new developers: Tweepy and xdk are free, but every read spends prepaid credits at $0.005 per post. Sorsa API starts every account with 100 free requests that never expire and need no card. From Python they collect 2,000 tweets by search, or up to 10K tweets in batches of 100.

Which Twitter API should I use with Python?

For reading X data from Python, use Sorsa API: one API key with requests instead of OAuth, search results from $0.07 per 1,000 tweets and batches from $0.014, 20 requests per second on every plan, search back to 2006, and no X login, so your account is never at risk. To post as well, pair it with the official X API.

Can I get Twitter data in Python without a developer account?

Yes. Sorsa API needs only its own key: sign up, set the ApiKey header and call any of its 45 endpoints from Python, with no X developer account, app review or OAuth. Libraries that read X by logging in with your username or browser cookies put that account at risk. The trade-offs are compared in Twitter API without a developer account.

Can I post tweets from Python with Sorsa API?

No. Sorsa API is read-only by design: it reads public posts, profiles and followers and never posts, likes, follows or sends messages, so a key can't act on any X account. To post from Python, use the official X API with an OAuth user token, such as Tweepy's create_tweet at $0.015 a post, and keep Sorsa API for the reads.

Is there a Python SDK or pip package for Sorsa API?

No package is needed: Sorsa API is plain REST with JSON, so requests or httpx covers every endpoint, and the helpers in this guide fit in about 40 lines. The live OpenAPI reference at api.sorsa.io/v3/swagger.json lists all 45 endpoints, so a generator such as openapi-generator can build a typed client if you want one.

#How we checked

We wrote every request in this guide from Sorsa API's live OpenAPI reference (45 endpoints) and its docs on rate limits, pagination and errors, read on November 2, 2026. We had no Sorsa API key for this pass, so the response shown uses demo values, and we ran each Python block against a local mock that answers in the documented shapes: the retries, the cursor loop, batches of 100, the pandas and csv exports, and the asyncio pacer: at 19 starts a second it ran 31 lanes with no 429, while at exactly 20 timing jitter sent one or two requests to the retry. The same day we read the official X API's prices, access steps and search and timeline limits on docs.x.com, X's notice on billing expansions, and the PyPI pages of Tweepy and xdk.

By the Sorsa API team. We build and run Sorsa API, the read-only Twitter/X API and scraper, in production since 2022 (formerly TweetScout API) with 6B+ requests served. About Sorsa API

What changed
  • Rewritten from scratch: today's official X API prices with the November 22 expansion billing, Tweepy 4.17.0 and xdk 0.10.6, and a Python path with one API key: retries, cursors, batches of 100, pandas and CSV, asyncio at 20 requests per second and a Tweepy method map.
  • First published.