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:
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
-
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.
-
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" -
Search for tweets. Send a query in X's search syntax to
POST /search-tweets(opens in a new tab) with the key in theApiKeyheader.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]) -
Read the page. Each response holds up to 20 tweets and a
next_cursorfor 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:
| Field | What it holds | Example |
|---|---|---|
id | The tweet's 19-digit ID, sent as a string: keep it one | 1985432176540981234 |
full_text | The whole text of the post | Loaded a week of #python posts |
created_at | When it was posted, ISO 8601 in UTC | 2026-11-01T18:42:07Z |
likes_count, retweet_count, reply_count, quote_count | Engagement counts | 42 |
view_count, bookmark_count | Views and bookmarks | 3810 |
user | The author's profile: 19 fields such as username and followers_count | py_data_notebook, 1840 |
quoted_status, retweeted_status | The quoted or reposted tweet, in the same shape | A 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
usernameandfollowers_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.
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.
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.
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)
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.
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 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, writeresults = await main(queries)instead.
The planner turns a job into requests, time on one key and cost at Sorsa API's lowest rate.
- Requests
- 5,000
- Fastest on one key
- about 4 min
- Sorsa API
- from $7
- 100 free requests
- 2,000
20 tweets per request
at 20 requests a second
at $1.40 per 1,000 requests
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.
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.
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 method | Endpoint | What changes |
|---|---|---|
search_recent_tweets | POST /search-tweets | Any date back to 2006, not only the last 7 days |
search_all_tweets | POST /search-tweets | The same endpoint with since: and until: in the query |
get_users_tweets | POST /user-tweets | A username is enough, and there is no 3,200-post cap |
get_tweets | POST /tweet-info-bulk | 100 IDs or links per request |
get_users | GET /info-batch | 100 usernames per request |
get_users_followers | GET /followers | 200 followers per page |
get_retweeters | POST /retweeters (opens in a new tab) | The same cursor loop |
create_tweet | none | Read-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:anduntil: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.
Sources
- X API pay-per-usage pricing (docs.x.com) (opens in a new tab)
- Getting Access (docs.x.com) (opens in a new tab)
- Tools and libraries: the official Python SDK (docs.x.com) (opens in a new tab)
- X API PPU: expansions update (X Developers, October 23, 2026) (opens in a new tab)
- Search Posts (docs.x.com) (opens in a new tab)
- tweepy on PyPI (opens in a new tab)
- xdk on PyPI (opens in a new tab)
- Sorsa API reference, OpenAPI
- Sorsa API rate limits (opens in a new tab)
- Sorsa API pagination (opens in a new tab)