TwitterAPI.io is an independent third-party service. Not affiliated with X Corp.

Twint (Twitter scraper): why it stopped working and how to migrate

By TwitterAPI.io Team•17 min read
Migration map diagram: twint CLI flags and Config attributes on the left (-u / c.Username, -s / c.Search, --since --until, --min-likes, -l / c.Lang, --near / -g, --followers, --following, --limit, --csv --json --database, --favorites) joined by arrows to the twitterapi.io endpoint or search operator that replaces each on the right (/twitter/user/last_tweets, /twitter/tweet/advanced_search, since: until:, min_faves:, lang:, near: best effort, /twitter/user/followers, /twitter/user/followings, pages x 20 via next_cursor, write your own, no equivalent).
The migration map this page follows: each twint flag or Config attribute on the left, the endpoint or search operator that replaces it on the right. Diagram, not measured data.

People still search for twint because for about five years it was the answer to "how do I get tweets without the Twitter API". Its README promised "Can fetch almost all Tweets (Twitter API limits to last 3200 Tweets only); Fast initial setup; Can be used anonymously and without Twitter sign up; No rate limitations", and for research, OSINT and thesis work that was true enough: twint -u handle --since 2019-01-01 -o out.csv --csv and you had a spreadsheet. Those tutorials, notebooks and Stack Overflow answers are still indexed, so the install still happens, and the first run still fails.

This page is the migration guide those tutorials now need. It is not a general survey of scraping options; that lives in the hub page on scraping Twitter without the official API, linked below. It is specifically about twint: what it was, why it stopped, and how each flag and twint.Config() attribute maps onto a request against https://api.twitterapi.io with an X-API-Key header. The wrapper at the end keeps twint's function shape and CSV columns so the rest of your pipeline does not have to change.

Everything quoted about twint comes from the archived repository, its wiki and its PyPI page as read this year; the price and response-shape facts come from docs.twitterapi.io and docs.x.com. Where the honest answer is "this does not migrate", the page says so.

01 — Section

What twint was, and why people still search for it

twint (Twitter Intelligence Tool) was a Python package by Cody Zacharias, described on PyPI as "An advanced Twitter scraping tool written in Python that allows for scraping Tweets from Twitter profiles without using Twitter's API." It worked by requesting the same HTML and JSON that Twitter's logged-out web search and profile pages loaded, parsing the results, and writing them to a file, a SQLite database, Pandas or Elasticsearch. It needed no developer account, no OAuth app and no login, which is exactly what made it popular with students, journalists and OSINT analysts who could not get or afford API access.

The interface had two halves. The CLI: twint -u username, twint -s pineapple, twint -u username --since 2015-12-20, twint -u username -o file.csv --csv, twint -u username --database tweets.db, twint -u username --followers, twint -u username --following, twint -u username --favorites (all from the README's examples). And the module: c = twint.Config(), then c.Username, c.Search, c.Since, c.Until, c.Limit, c.Store_csv, c.Output, c.Lang, c.Near, c.Geo, c.Min_likes, c.Min_retweets, c.Database and so on, followed by twint.run.Search(c), twint.run.Followers(c) or twint.run.Following(c). The wiki's Configuration page lists 62 attributes; most jobs used six.

The repository's own scale shows why the queries persist: 16.4k stars, 2.7k forks, 845 commits, a Python 3.6 to 3.8 badge, and thousands of downstream notebooks that pip install twint in the first cell. The search volume for "twint" is people re-running those notebooks and getting an error, which is the next section.

02 — Section

Why twint stopped working: the guest token, the noData error, and the archive

twint never used an API key. To read search results it first fetched a guest token from Twitter's public HTML, then called the internal JSON endpoints the web app used, with that token. Both halves of that flow were X's to change, and both changed.

The guest token went first. Issue #1433, [ERROR] twint.token.RefreshTokenException: Could not find the Guest token in HTML, was opened on March 3, 2023; a commenter in the thread wrote that "from 28 feb 2023 it always return Raising Exception Could not find the Guest token in HTML". The suggested workaround was to install the development branch and replace token.py with a patched version from a gist. That is the state the project was in when the owner archived it on March 30, 2023.

The feed endpoints went next. The error most people quote, CRITICAL:root:twint.run:Twint:Feed:noData, is older than the archive: the same string appears in issue titles from #587 (February 2020, noDataExpecting) through #896 (noData'items_html'), #992, #1023, #1130, #1217, #1257, #1287, #1311 (noData'globalObjects') to #1434, opened on March 23, 2023, where a maintainer's reply was "maybe twitter modified something". Each of those was one response field twint parsed disappearing from Twitter's front end; each time, a patch followed. After the archive there was nobody to patch it.

Then X closed the door for logged-out clients. On June 30, 2023 X began requiring a login to view posts at all; the July 5, 2023 TechCrunch report quotes Elon Musk's reason, "We were getting data pillaged so much that it was degrading service for normal users!", and the read limits that came with it (1,000 posts a day for unverified accounts, 10,000 for verified). The login wall on single posts was lifted days later, but the logged-out search and timeline JSON that twint scraped did not come back: the endpoints twint called no longer respond for a client without an authenticated session, which is why every fork that only patches the token step still ends in noData.

So the diagnosis for a twint not working search is short. It is not your Python version, your proxy or your pip cache. The last PyPI release is 2.1.20 from April 2020; the GitHub master branch is read-only; the data source is gone. The rest of this page is what to do about it.

03 — Section

Three routes exist. The right one depends on how much of twint's "no account, no API" promise you need to keep, and how much of your week you want to spend keeping it running.

RouteWhat it isWhat it needsWhat breaks it
Patched twint forkAny of the GitHub forks that swap token.py or point at a Nitter instancePython, luckThe next front-end change; most forks stopped at the same noData wall in 2023
Cookie-based library (twscrape, Twikit and similar)Python libraries that log in with one or more X accounts and call the GraphQL endpoints the web app usesReal X accounts whose cookies you supply, a proxy per account, a rotation strategyX rate-limits, locks or suspends the accounts; the GraphQL query IDs change; you own all of that
Hosted read API (this page)GET https://api.twitterapi.io plus the endpoint path, with an X-API-Key header; JSON outAn API key from the dashboard; no X account, no OAuth appPay per post returned: 15 credits each, $0.15 per 1,000
Official X API (pay-per-usage)api.x.com v2 with an approved developer app and OAuthDeveloper account, app approval, card on file$0.005 per post read and $0.010 per user read, capped at 3 million post reads a month (docs.x.com pricing)

twscrape and Twikit are the closest in spirit to twint, and they are worth naming plainly: they work by acting as logged-in accounts, so the account risk that twint never had is now yours, and the maintenance burden that killed twint (a parser chasing X's front end) is still there, just moved into a better-maintained library. If your twint job was a one-off thesis pull, that may be fine. If it was a scheduled job, the arithmetic in the cost section usually says to pay for the read instead.

The rest of this guide assumes the hosted-API route, because it is the one that maps onto twint's flags without you operating accounts. The official X API is the other no-maintenance option; it is included in the tables so you can see what it charges for the same reads.

04 — Section

The migration table: every twint flag and Config attribute, mapped to an endpoint

Each row is one twint CLI flag (from the README and the wiki's Basic usage page) with its twint.Config() attribute, and the request that replaces it. Endpoint paths are relative to https://api.twitterapi.io, all are GET with the X-API-Key header, and the response shapes are the ones in docs.twitterapi.io: advanced_search returns a top-level tweets array with has_next_page and next_cursor; last_tweets nests the list at data.tweets with the same two pagination fields at the top level; followers and followings return followers[] / followings[] plus the same pagination pair.

twint flag / ConfigWhat it didtwitterapi.io replacement
-u username / c.UsernameScrape a user's timelineGET /twitter/user/last_tweets?userName=<user> (newest first, up to 20 per page, data.tweets), or advanced_search with query=from:<user> when you also need date bounds
c.User_idSame, by numeric IDlast_tweets?userId=<id> (the docs recommend userId as more stable than userName)
-s "term" / c.SearchSearch tweetsGET /twitter/tweet/advanced_search?query=<term>&queryType=Latest&cursor= (top-level tweets[], has_next_page, next_cursor)
-u user -s termSearch within one user's tweetsadvanced_search with query=from:<user> <term>
--since YYYY-MM-DD / c.SinceTweets on or after a datesince:YYYY-MM-DD inside the query string (UTC)
--until YYYY-MM-DD / c.UntilTweets before a dateuntil:YYYY-MM-DD inside query (UTC, exclusive); for a time of day use since_time: / until_time: with Unix seconds, because the _HH:MM:SS_UTC suffix is not supported
--year 2014 / c.YearTweets before a yearuntil:2014-01-01
--limit N / c.LimitNumber of tweets, in increments of 20Pages x 20: stop paginating when you have N rows or has_next_page is false
-l xx / c.LangLanguage filterlang:xx operator
--min-likes N / c.Min_likesEngagement floormin_faves:N; --min-retweets -> min_retweets:N; --min-replies -> min_replies:N
-fr / c.Filter_retweetsExclude retweets-filter:retweets; -filter:replies for replies
--links / c.Links, --images, --videos, --mediaContent classfilter:links, filter:images, filter:videos, filter:media (negate with -)
-pt / c.Popular_tweetsPopular instead of recentqueryType=Top instead of Latest
--to user / c.ToTweets sent to a userto:<user> operator; for a reply list on one post, GET /twitter/tweet/replies?tweetId=
--near "City" / c.Near, -g lat,lon,km / c.GeoLocation searchnear: / geocode: are best effort on X's current index; see the search-by-location guide for the four signals that still work
--followers / twint.run.FollowersA user's followersGET /twitter/user/followers?userName=<user>&pageSize=200 (followers[], screen_name, followers_count, created_at)
--following / twint.run.FollowingWho a user followsGET /twitter/user/followings?userName=<user>&pageSize=200 (followings[])
--user-full / c.User_fullFull profile per userAlready in each follower object; for one handle, GET /twitter/user/info?userName= (data.followers, data.statusesCount, data.createdAt)
--favorites / twint.run.FavoritesTweets a user likedNot offered: no read endpoint here returns another user's likes (see FAQ)
--retweets / c.RetweetsRetweets by the userIncluded in last_tweets; each retweet carries a retweeted_tweet object
--profile-fullSlow full-timeline walklast_tweets with next_cursor; for older history, from:<user> searches in since: / until: windows
--resume file / c.ResumeResume a crawlPersist next_cursor yourself and pass it as cursor=
--email, --phoneRegex on tweet textRun the same regex on text client-side
-o file --csv / --json / --database / -esStorageWrite your own: the wrapper below writes CSV and JSON; the SQLite block replaces --database

Three details to carry into the code. First, until: is exclusive, so --since 2024-01-01 --until 2024-02-01 in twint becomes since:2024-01-01 until:2024-02-01 and returns January. Second, advanced_search pages are "up to 20" posts because ads and non-tweet cards are filtered out, so do not assume exactly 20 per page when counting toward --limit. Third, X search operators work inside query only on advanced_search; last_tweets takes no operators, which is why the -u user --since combination moves to from:<user> since: on search.

05 — Section

Search operators for the twint -s migration: since, until, lang, min_faves

advanced_search takes the same grammar as the X search box, which is also the grammar twint assembled behind its flags. The parameters are query (the operator string), queryType (Latest or Top, default Latest) and cursor (empty string for page one). Billing is 15 credits per post returned, so a full page of 20 is $0.003 and an empty page is the 15-credit minimum, $0.00015.

Date windows. since:YYYY-MM-DD until:YYYY-MM-DD are UTC and the until date is exclusive. twint users often chained --since with a time (--since "2015-12-20 20:30:15" is in the README); here the dated form with a time suffix is explicitly unsupported, so use since_time:<unix> and until_time:<unix> for anything finer than a day. The guide on searching tweets by date (linked below) covers the gotchas, including what happens when since: is in the future.

Author and audience. from:<user> replaces -u on a search, to:<user> replaces --to, and @user finds mentions. Combining them is where twint's -u user -s term lands: from:user term.

Engagement floor. min_faves:N, min_retweets:N and min_replies:N replace --min-likes, --min-retweets and --min-replies. They are also the cost lever: because you pay per post returned, a floor that halves the result set halves the bill. The search operators reference linked below shows which operators our customers actually use, with shares.

Language and content class. lang:en for -l en; -filter:retweets and -filter:replies for -fr; filter:links, filter:images, filter:videos, filter:media for twint's --links, --images, --videos, --media. twint's --verified has no tested equivalent in our operator list, so treat it as unmigrated.

Location. twint's --near and -g built near: and geocode: into the query. Those operators are not in X's current operator reference and are honoured at best on a geotag-only index, so a near: query can come back empty while the same query without it fills a page. The search-by-location guide walks through the four location signals that still work (post Place, profile location string, the account-based-in label, place names in text) and the control-query check that tells you an operator was ignored.

Keep the whole string under 512 characters, which X's operator reference gives as the self-serve query limit: in practice about 20 from: clauses OR-ed together, which is how a twint loop over a list of handles collapses into one query.

06 — Section

Followers, following and favorites: what migrates and what does not

twint -u user --followers and --following printed usernames, or full profiles with --user-full. The replacements return full profiles by default: GET /twitter/user/followers?userName=<user>&cursor=&pageSize=200 and GET /twitter/user/followings?userName=<user>&cursor=&pageSize=200. pageSize is 20 to 200 (default 200); each page carries has_next_page and next_cursor; and each user object is snake_case with id, screen_name, name, followers_count, friends_count, statuses_count, created_at, location, description, protected, verified. Note the casing difference from /twitter/user/info, which uses userName and createdAt.

Billing is 1 to 3 credits per user returned, tiered by page size: 1 credit per user when you ask for the full 200, more per user on smaller pages, with a 60-credit minimum per call (docs.twitterapi.io, followers endpoint). Request 200 at a time and a 10,000-follower list is 50 calls and about $0.10. The official X API prices the same read at $0.010 per follower or following returned (docs.x.com pricing), so that list is $100 there.

--favorites does not migrate. twint scraped the public likes tab, which X removed from other users' profiles in 2024; no read endpoint on twitterapi.io returns another account's likes, and none is listed in the endpoint manifest. The write side has /twitter/like_tweet_v2 for liking as your own logged-in account, which is a different product and is not what --favorites did. If a pipeline depended on likes, the honest replacement is engagement counts on the posts themselves (likeCount on every tweet object), not a likes list per user.

The followers code is short enough to show in full. It keeps twint's newest-first order and writes the columns twint's --user-full output had.

python
"""twint -u username --followers --user-full  ->  /twitter/user/followers (newest first).
Swap the URL for /twitter/user/followings for twint --following. Each page returns up to
200 profiles in snake_case (screen_name, followers_count, created_at); 1 credit per user
at pageSize=200, so a full page is $0.002."""
import csv
import os

import requests

HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}


def twint_followers(username, limit=1000):
    out, cursor = [], ""
    while len(out) < limit:
        r = requests.get("https://api.twitterapi.io/twitter/user/followers",
                         params={"userName": username, "cursor": cursor, "pageSize": 200},
                         headers=HEADERS, timeout=30)
        r.raise_for_status()
        body = r.json()
        if body.get("status") == "error":
            raise SystemExit(f"followers: {body.get('msg')}")
        users = body.get("followers") or []
        out.extend({"id": u["id"], "username": u["screen_name"], "name": u.get("name"),
                    "followers": u.get("followers_count"), "following": u.get("friends_count"),
                    "tweets": u.get("statuses_count"), "join_date": u.get("created_at"),
                    "bio": u.get("description"), "location": u.get("location")} for u in users)
        if not users or not body.get("has_next_page"):
            break
        cursor = body["next_cursor"]
    return out[:limit]


rows = twint_followers("NASA", limit=400)
with open("nasa_followers.csv", "w", newline="", encoding="utf-8") as f:
    w = csv.DictWriter(f, fieldnames=list(rows[0].keys()) if rows else ["id"])
    w.writeheader()
    w.writerows(rows)
print(len(rows), "followers -> nasa_followers.csv")
07 — Section

The twint-compatible wrapper: twint_search() and twint_user() with CSV, JSON and SQLite

The main code block at the end of this page is a drop-in for the two twint entry points most pipelines used. twint_search(query, since, until, limit) is twint -s with --since, --until, --limit and -pt; twint_user(username, limit) is twint -u. Both paginate with next_cursor, both stop at limit or when has_next_page is false, and both return a list of dicts with twint's column names: id, date, username, tweet, replies_count, retweets_count, likes_count, link. Those map from the API's id, createdAt, author.userName, text, replyCount, retweetCount, likeCount and url.

Two mapping notes. createdAt on tweet objects is the classic format Tue Dec 10 07:00:30 +0000 2024, always with a +0000 offset, so the wrapper parses it with %a %b %d %H:%M:%S %z %Y and prints YYYY-MM-DD HH:MM:SS in UTC, which is what twint's date and time columns held. And last_tweets wraps errors as status: "error" with the reason in msg while advanced_search has no wrapper (the page is just tweets, has_next_page, next_cursor), so the paginator checks both shapes.

--database tweets.db becomes the block below: a SQLite table keyed on the post ID with INSERT OR REPLACE, so re-running a window is idempotent, which is what --resume was for. twint's own table had many more columns; add the ones you used (viewCount, quoteCount, bookmarkCount, lang, isReply, conversationId are all on each tweet object) and keep the rest out.

python
"""twint --database tweets.db equivalent: a minimal SQLite store for the rows the wrapper
returns. INSERT OR REPLACE keyed on the post id makes re-runs idempotent, which is what
twint's --resume was for. The schema is deliberately small; twint's own tweets table had
many more columns."""
import sqlite3

SCHEMA = """CREATE TABLE IF NOT EXISTS tweets (
    id TEXT PRIMARY KEY, date TEXT, username TEXT, tweet TEXT,
    replies_count INTEGER, retweets_count INTEGER, likes_count INTEGER, link TEXT)"""


def store_sqlite(rows, path="tweets.db"):
    con = sqlite3.connect(path)
    con.execute(SCHEMA)
    con.executemany(
        "INSERT OR REPLACE INTO tweets VALUES "
        "(:id, :date, :username, :tweet, :replies_count, :retweets_count, :likes_count, :link)",
        rows)
    con.commit()
    n = con.execute("SELECT COUNT(*) FROM tweets").fetchone()[0]
    con.close()
    return n


# rows = twint_search("pineapple", since="2026-01-01", limit=200)
# print(store_sqlite(rows), "rows in tweets.db")
08 — Section

Cost: twint was free, so what does the API job cost, and what would the official X API charge?

twint cost nothing in money and a great deal in time: proxies or Tor when Twitter throttled an IP, a parser patch every few months, and in the end a total loss. The replacement is metered, so here is the meter. Prices are from the twitterapi.io pricing page and the official X API pay-per-usage page; the ratios are arithmetic, not a benchmark.

Workload (the twint job you had)twitterapi.ioOfficial X API pay-per-usage
Per unit15 credits per post = $0.15 per 1,000 posts; 15-credit minimum per request; profiles $0.18 per 1,000; followers 1 to 3 credits each$0.005 per post read; $0.010 per user read; $0.010 per follower or following read; 3 million post reads a month cap
One full search page (20 posts)$0.003$0.10
twint -s term --limit 1000 (50 pages)$0.15$5.00 (about 33x: $5.00 / $0.15)
1,000 posts a day, every day$0.15 a day, $4.50 a month$5 a day, $150 a month
10,000 posts a day (a typical scheduled twint job)$1.50 a day, $45 a month$50 a day, $1,500 a month
100,000 posts a day$15 a day, $450 a month$500 a day, $15,000 a month, and at the 3M-a-month cap
10,000 followers (--followers on a mid-size account)50 calls of 200 at 1 credit each, about $0.10$100
SetupAPI key from the dashboard; no X accountDeveloper account, approved app, OAuth, card on file
Rate0.2 requests/s before the first payment; 20 once paid; 50 on Scale; 500 on BusinessPer-endpoint limits set by X

The worked example in the direct answer is the middle row: a twint job that pulled 10,000 posts a day is 10,000 x 15 = 150,000 credits = $1.50 a day at $1 per 100,000 credits, or $45 over 30 days. The official route for the same reads is 10,000 x $0.005 = $50 a day, $1,500 a month. New accounts that pass the automatic bot check get $1 of free credits (100,000 credits, no card; one per person or network; the credits expire after 31 days), which covers about 6,600 posts, enough to run the wrapper against your old twint query and compare the CSVs before paying anything. Subscriptions start at Starter $29 a month; the pricing page has the tiers.

One rule that changes how you write the loop: because billing is per post returned, a search that returns nothing costs only the 15-credit minimum, so narrow windows are cheap. A daily from:user since:<yesterday> pull on a quiet account costs $0.00015 on the days it finds nothing, and the full $0.003 only on a day it fills a page.

Grouped bar chart on a log scale of monthly cost for a migrated twint job at 1,000, 10,000 and 100,000 posts a day: twitterapi.io at $4.50, $45 and $450 a month versus the official X API pay-per-usage at $150, $1,500 and $15,000 a month, computed from $0.15 per 1,000 posts and $0.005 per post read over 30 days.
Monthly cost of the reads a twint job used to make, computed from the two published price lists over 30 days ($0.15 per 1,000 posts vs $0.005 per post read). Arithmetic, not a measurement; the 100,000-a-day case sits at the official plan's 3M-reads-a-month cap.
python
"""twint-compatible wrapper over twitterapi.io (reads only, no X login).

twint_search(query, since, until, limit)  <-  twint -s "term" --since --until --limit  /  c.Search
twint_user(username, limit)               <-  twint -u username --limit            /  c.Username

Both paginate with next_cursor, map every post to twint's familiar CSV columns
(id, date, username, tweet, replies_count, retweets_count, likes_count, link)
and write CSV or line-delimited JSON, like twint --csv / --json.

Needs TWITTERAPI_IO_KEY in the environment (key from twitterapi.io/dashboard).
Cost: 15 credits per post returned = $0.15 per 1,000 posts; a full 20-post page is $0.003,
and a page that returns nothing is billed the 15-credit minimum ($0.00015).
"""
import csv
import json
import os
import sys
from datetime import datetime

import requests

HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}
CREATED_AT = "%a %b %d %H:%M:%S %z %Y"      # e.g. "Tue Dec 10 07:00:30 +0000 2024", always +0000
COLUMNS = ["id", "date", "username", "tweet", "replies_count",
           "retweets_count", "likes_count", "link"]


def to_twint_row(t):
    """Map one twitterapi.io tweet object onto twint's CSV columns."""
    created = datetime.strptime(t["createdAt"], CREATED_AT)
    return {
        "id": t["id"],
        "date": created.strftime("%Y-%m-%d %H:%M:%S"),          # UTC, like twint's date + time
        "username": (t.get("author") or {}).get("userName"),
        "tweet": t.get("text", ""),
        "replies_count": t.get("replyCount", 0),
        "retweets_count": t.get("retweetCount", 0),
        "likes_count": t.get("likeCount", 0),
        "link": t.get("url") or t.get("twitterUrl"),
    }


def _paginate(url, params, limit):
    """Follow next_cursor until `limit` rows or has_next_page is false.
    advanced_search puts `tweets` at the top level; last_tweets nests them in data.tweets
    and wraps errors as status == "error" with the reason in msg."""
    rows, cursor = [], ""
    while len(rows) < limit:
        r = requests.get(url, params={**params, "cursor": cursor}, headers=HEADERS, timeout=30)
        r.raise_for_status()
        body = r.json()
        if body.get("status") == "error":
            raise SystemExit(f"{url}: {body.get('msg')}")
        tweets = body.get("tweets")
        if tweets is None:
            tweets = (body.get("data") or {}).get("tweets") or []
        rows.extend(to_twint_row(t) for t in tweets)
        if not tweets or not body.get("has_next_page"):
            break
        cursor = body["next_cursor"]
    return rows[:limit]


def twint_search(query, since=None, until=None, limit=500, popular=False):
    """twint -s "query" [--since YYYY-MM-DD] [--until YYYY-MM-DD] [--limit N] [-pt]
    since:/until: are UTC and the until date is exclusive; use since_time:/until_time:
    with Unix seconds for finer bounds (the _HH:MM:SS_UTC suffix is not supported)."""
    q = query
    if since:
        q += f" since:{since}"
    if until:
        q += f" until:{until}"
    return _paginate("https://api.twitterapi.io/twitter/tweet/advanced_search",
                     {"query": q, "queryType": "Top" if popular else "Latest"}, limit)


def twint_user(username, limit=500, include_replies=False):
    """twint -u username [--limit N]. Newest first, 20 per page, retweets included
    (each carries a retweeted_tweet object). Pass userId= instead of userName= in
    production: the docs recommend it as more stable."""
    return _paginate("https://api.twitterapi.io/twitter/user/last_tweets",
                     {"userName": username, "includeReplies": str(include_replies).lower()}, limit)


def store_csv(rows, path):                      # twint -o file.csv --csv
    with open(path, "w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=COLUMNS)
        w.writeheader()
        w.writerows(rows)


def store_json(rows, path):                     # twint -o file.json --json (one object per line)
    with open(path, "w", encoding="utf-8") as f:
        for row in rows:
            f.write(json.dumps(row, ensure_ascii=False) + "\n")


if __name__ == "__main__":
    # twint -s pineapple --since 2026-01-01 --limit 100 -o pineapple.csv --csv
    rows = twint_search("pineapple -filter:retweets", since="2026-01-01", limit=100)
    store_csv(rows, "pineapple.csv")
    print(f"search: {len(rows)} posts -> pineapple.csv (about ${len(rows) * 15 / 100_000:.4f})")

    # twint -u NASA --limit 100 -o nasa.json --json
    handle = sys.argv[1] if len(sys.argv) > 1 else "NASA"
    rows = twint_user(handle, limit=100)
    store_json(rows, f"{handle}.json")
    if rows:
        print(f"user: {len(rows)} posts, newest {rows[0]['date']}, oldest {rows[-1]['date']} -> {handle}.json")
09 — Questions

Questions readers ask

Is twint still working in 2026?

No. The twintproject/twint repository was archived by its owner on March 30, 2023 and is read-only, the last PyPI release (2.1.20) is from April 2020, and the logged-out guest endpoints it scraped stopped responding in early 2023. Runs end in RefreshTokenException: Could not find the Guest token in HTML or CRITICAL:root:twint.run:Twint:Feed:noData. Patching token.py does not bring the data source back.

Why does twint show CRITICAL:root:twint.run:Twint:Feed:noData?

That line means a field twint expected in Twitter's response (globalObjects, items_html, tweet) was not there. It appears in repository issues from #587 in 2020 to #1434 in March 2023, each time after a front-end change, and each time a patch followed until the project was archived. Since mid-2023 the endpoints behind it no longer answer logged-out clients at all, so the error is permanent.

What is twint2, and is there an official successor?

There is no official successor from the twintproject organisation; the main repository is archived. What people call twint2 or a twint fork is one of the community forks on GitHub that swap the token logic or point at a Nitter instance, and those hit the same wall. The maintained options are cookie-based libraries such as twscrape or Twikit, which act as logged-in X accounts you supply (with the account risk that implies), or a hosted read API like the one mapped on this page.

Is using twint, or scraping Twitter (X), legal and safe?

Reading public posts is generally lawful in many jurisdictions, but X's Terms of Service and its automation rules (help.x.com/en/rules-and-policies/x-automation) govern how you access the platform, and data protection law applies to personal data regardless of how public it was. The practical safety difference is where the risk sits: twint and cookie-based libraries act as a client X did not authorise, and with cookie libraries the accounts you supply can be rate-limited, locked or suspended. A hosted read API needs no X account on your side. Collect the minimum your question needs either way.

Can I get a user's liked tweets like twint --favorites?

Not through a read endpoint here. twint scraped the public likes tab, which X removed from other users' profiles in 2024, and twitterapi.io has no endpoint that returns another account's likes. The engagement counts on each post (likeCount, retweetCount, replyCount, viewCount) are returned on every tweet object, which covers the usual analysis need.

How do I get more than 3,200 tweets from one user?

twint's README noted that timeline scrolling capped out around 3,200 posts. On twitterapi.io, GET /twitter/user/last_tweets is cursor-based: keep passing next_cursor until has_next_page is false; the docs do not state a fixed cap. For older history, or for a bounded window, use GET /twitter/tweet/advanced_search with from:<user> since:YYYY-MM-DD until:YYYY-MM-DD and walk the windows; search reaches whatever X's index serves for that query.

How do I search tweets by date, like twint --since and --until?

Put the operators inside the query string on GET /twitter/tweet/advanced_search: term since:2024-01-01 until:2024-02-01. Dates are UTC and until: is exclusive. For a time of day use since_time: and until_time: with Unix seconds; the since:2021-12-31_23:59:59_UTC form is not supported. last_tweets takes no operators, so a dated pull on one account is from:<user> since: until: on search.

Do I need an X account or a developer app to replace twint with an API?

Not for reads. twitterapi.io reads need only an API key from the dashboard and an X-API-Key header; there is no OAuth app, no X login and no approval step. The official X API needs a developer account, an approved app and a card on file, and charges $0.005 per post read against $0.15 per 1,000 posts here.

10 — Further reading

Continue

Sources & further reading
More from this series
Build it

Stop reading. Start building.

Starter credits cover real testing on real data. Google sign-in, no card, no application queue.

Get an API key