This guide downloads the tweets of one X account with Python: first a single page, then the whole timeline, then a CSV file and a script you can run every day that only asks for tweets it has not seen. It uses TwittrAPI, a third-party API for public X data, so you need an API key from us but no X developer account and no X login. The same steps are shown for Node.js at the end.

What you get back is what X shows on the profile, newest first. Replies are left out unless you ask for them, and deleted or withheld tweets are not returned.

Step 1: Get an API key

Create a key on /start. New accounts get $0.10 of test credit for 14 days, which is about 1,100 calls. Put the key in an environment variable so it stays out of your code:

export TWITTRAPI_API_KEY="your-key"

Every request sends it in the X-API-KEY header. One request is one billed call, whatever it returns; the pricing page has the rates, from $0.065 per 1,000 calls.

Step 2: Fetch the first page of tweets

The endpoint is GET /user/{userId}/tweets. It takes a numeric user ID, but you can pass -1 as the ID and the handle as username instead. With format=simple you get flat JSON with a fixed set of fields (simple format).

With nothing but requests:

import os
 
import requests
 
API = "https://api.twittrapi.com"
HEADERS = {"X-API-KEY": os.environ["TWITTRAPI_API_KEY"]}
 
r = requests.get(
    f"{API}/user/-1/tweets",
    headers=HEADERS,
    params={"username": "NASA", "count": 20, "format": "simple"},
    timeout=30,
)
r.raise_for_status()
page = r.json()
 
for t in page["data"]:
    if t.get("unavailable"):
        continue
    print(t["createdAt"], t["counts"]["likes"], t["text"][:100])
 
print("more pages:", page["hasNextPage"])

Each tweet has id, url, text, createdAt, lang, author, counts (likes, retweets, replies, quotes, bookmarks, views), media, urls, hashtags, replyTo, quoted and retweetOf. The field table lists them all.

Step 3: Follow the cursor to get all pages

Every page carries nextCursor. Send it back as cursor to get the next page. When hasNextPage is false, you have everything X will return.

Paging through tweets with a cursor

Call 1

GET /user/-1/tweets

→ ~20 tweets

nextCursor: "A1"

Call 2

?cursor=A1

→ ~20 tweets

nextCursor: "B2"

Call n

?cursor=…

→ last tweets

hasNextPage: false

Every page is one call, billed at $0.065 to $0.09 per 1,000 calls. X decides how many tweets a page holds (about 20), so loop until hasNextPage is false instead of counting.
def all_tweets(username: str, max_pages: int = 200):
    """Yield every tweet on the profile, newest first."""
    cursor = None
    for _ in range(max_pages):
        params = {"username": username, "count": 100, "format": "simple"}
        if cursor:
            params["cursor"] = cursor
        r = requests.get(f"{API}/user/-1/tweets", headers=HEADERS, params=params, timeout=30)
        r.raise_for_status()
        page = r.json()
        for t in page["data"]:
            if not t.get("unavailable"):
                yield t
        if not page["hasNextPage"]:
            return
        cursor = page["nextCursor"]

max_pages is a safety limit: every page is a billed call, so a typo in a loop should not run forever.

How far back this goes is up to X. Profile timelines stop after a few thousand tweets; scraper projects such as twscrape report about 3,200. For older tweets, use search with from:NASA until:2020-01-01, which walks back by date instead.

Step 4: Use the Python SDK instead (optional)

The twittrapi package wraps the same calls. It reads the key from TWITTRAPI_API_KEY, retries 429 and 5xx responses with backoff, and paginate follows the cursor for you:

pip install twittrapi
from twittrapi import TwittrAPI
 
client = TwittrAPI(response_format="simple")
 
tweets = []
for page in client.paginate(client.users.tweets, username="NASA", count=100, max_pages=50):
    tweets += [t for t in page["data"] if not t.get("unavailable")]
 
print(len(tweets), "tweets")

max_pages defaults to 10. Pass None to fetch until the cursor runs out. If you also want the account's replies, use client.users.tweets_and_replies (GET /user/{userId}/tweetsAndReplies); for photos and videos only, client.users.media (GET /user/{userId}/media).

Step 5: Save the tweets to CSV

Nested fields such as counts need to be flattened into columns. Keep the ID as text: spreadsheet apps round 19-digit numbers.

import csv
 
FIELDS = ["id", "createdAt", "text", "likes", "retweets", "replies", "quotes", "views", "lang", "url", "isReply", "media"]
 
with open("nasa_tweets.csv", "w", newline="", encoding="utf-8") as f:
    w = csv.DictWriter(f, fieldnames=FIELDS)
    w.writeheader()
    for t in tweets:
        c = t["counts"]
        w.writerow({
            "id": t["id"],
            "createdAt": t["createdAt"],
            "text": t["text"],
            "likes": c["likes"],
            "retweets": c["retweets"],
            "replies": c["replies"],
            "quotes": c["quotes"],
            "views": c["views"],
            "lang": t["lang"],
            "url": t["url"],
            "isReply": t["replyTo"] is not None,
            "media": " ".join(m["url"] for m in t["media"]),
        })

encoding="utf-8" matters: tweets contain emoji and non-Latin text. If Excel shows garbled characters, open the file through Data > From Text/CSV and pick UTF-8, or write it with encoding="utf-8-sig".

Step 6: Fetch only new tweets on later runs

For a daily job you do not need the whole timeline again. Timelines are newest first, so stop as soon as you reach a tweet you already have. A pinned tweet sits at the top of the first page regardless of its date, so skip it when deciding where to stop.

import json
from pathlib import Path
 
STATE = Path("nasa_last_id.json")
last_id = int(json.loads(STATE.read_text())["last_id"]) if STATE.exists() else 0
 
new = []
done = False
for page in client.paginate(client.users.tweets, username="NASA", count=100, max_pages=20):
    for t in page["data"]:
        if t.get("unavailable") or t["isPinned"]:
            continue
        if int(t["id"]) <= last_id:
            done = True
            break
        new.append(t)
    if done:
        break
 
if new:
    STATE.write_text(json.dumps({"last_id": max((t["id"] for t in new), key=int)}))
print(len(new), "new tweets")

Tweet IDs grow over time, so comparing them as integers is a safe way to tell old from new. A run with nothing new costs one call.

Node.js version

The twittrapi npm package has the same methods (Node 18 or later):

npm install twittrapi
import { TwittrAPI } from "twittrapi";
 
const client = new TwittrAPI({ format: "simple" }); // key from TWITTRAPI_API_KEY
 
const tweets = [];
for await (const page of client.paginate(
  client.users.tweets,
  { username: "NASA", count: 100 },
  { maxPages: 50 },
)) {
  for (const t of page.data.data) {
    if (!t.unavailable) tweets.push(t);
  }
}
 
console.log(tweets.length, "tweets");

Without the SDK, use fetch:

const res = await fetch(
  "https://api.twittrapi.com/user/-1/tweets?username=NASA&count=20&format=simple",
  { headers: { "X-API-KEY": process.env.TWITTRAPI_API_KEY } },
);
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
const page = await res.json();
console.log(page.data.length, page.hasNextPage, page.nextCursor);

Errors you may see

StatusMeaningWhat to do
401Missing or wrong key (not charged)Check the X-API-KEY header
402Balance used up or trial expired (not charged)Top up in the dashboard
404X says the account does not exist (charged)Check the handle with GET /user/by/username/{username}
429Over your requests per second (not charged)Wait and retry
5xxFailure on our side or between us and X (refunded)Retry with backoff

The full list is in error handling. Protected accounts return no tweets: the API only reads what a logged-out visitor could see.

Frequently asked questions

Can I get tweets from a Twitter account in Python without the official API?

Yes. A third-party API such as TwittrAPI returns public tweets of any account over plain HTTPS with its own key, so you do not need an X developer account or app. Scraping the website yourself also works at small scale, but X's terms forbid it without consent and it breaks often.

How many tweets can I get from one account?

As many as X still shows on the profile timeline, which ends after a few thousand tweets. For older tweets, search with from:handle and a since: and until: date range, one window at a time.

How much does it cost to download 3,000 tweets?

A page holds up to about 100 tweets when you ask for count=100, often fewer. 3,000 tweets take roughly 30 to 150 calls, which is about one cent or less at TwittrAPI's per-call prices.

Does this include replies and retweets?

The tweets endpoint returns the account's posts and retweets, without replies. Use the tweets-and-replies endpoint to include replies. Retweets have a retweetOf field with the original tweet.

Is twint or snscrape still an option in 2026?

Not for X. Both relied on logged-out access that X closed in 2023; their issue trackers have been full of "blocked" reports since then. Our post on scraping X in 2026 covers what still works.

Sources

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