Changelog¶
All notable changes to this project are documented in this file.
[5.8.0] - 2026-09-15¶
This release removes the silent failures that a user could meet, makes the first run obvious, and adds the newest versions of Python. Every change is additive: no method, no field and no default of 5.7.x changes.
Fixed¶
- A run no longer fails silently when the request signature cannot build. X answers HTTP 404 for a GraphQL request without the
x-client-transaction-idheader, and that 404 describes the request, not the account. The build of that header needs a live page of X; a transient network failure of that page left the header absent, and the run then rested the accounts one by one until it ended with no clear cause. Now the build retries with a backoff, a 404 rebuilds the header and retries the request on the same account before any cooldown, and a request that still cannot carry the header logs the reason. - A wrong or expired
auth_tokennow reports at once. Before, the constructor gave no warning and the first call failed with a message about cooldowns. The provisioning now counts the usable accounts, and the constructor warns when none is usable and names the likely cause. - A
cookies_fileaccepts every shape that an inlinecookies=accepts, including a file exported by a browser cookie extension. The two paths now share one loader, so the same data gives the same accounts. get_tweet_info,get_trendingandget_user_info(user_ids=...)raise a clear error on a refusal. A missing or deleted item still gives no row and no error, but a transport failure or a non-200 status no longer returns a silent empty list.
Added¶
- Python 3.13 and 3.14 are supported and tested. The test suite runs on Python 3.9 through 3.14.
- The package ships
py.typed. A type checker of a project that uses Scweet now reads the type hints of the library. - New
ScweetConfigfields for the request signature:transaction_init_attempts(3),transaction_init_backoff_s(1.5), andrequest_404_retries(1). ATransactionIdProvider.refresh()method rebuilds the header on demand.
Changed¶
- The README quickstart and the documentation teach the same first path: one
auth_token, one call, one result. The quickstart code block runs as written, with no placeholder proxy. A test pins the two documents to the same first path.
[5.7.0] - 2026-09-14¶
The read surface grows from 6 operations to 13, and the query-id refresh now covers every one of them. Every change is additive.
Added¶
get_tweet_info(tweet_ids)— full tweet records for ids you already hold, up to 50 ids in one request. A deleted id gives no row and no error.get_tweet_replies(tweet_id, limit=...)— the replies under one tweet, as full tweet records, without the focal tweet itself.get_reposters(tweet_id, limit=...)— the accounts that reposted one tweet, as user records.get_user_info(user_ids=[...])— profile lookup by numeric id, up to 100 ids in one request; combines with lookup by username. When X refuses the id endpoint for a connection (it answers HTTP 403 there at times), the call raises an error that names the refusal and the username alternative, instead of returning an empty list.search_users(query, limit=...)— the "People" tab of a search, as user records.get_trending()— the trends of the explore page: the name, the context line with the post count, and the raw item.get_profile_media(users, limit=...)— only the tweets of a profile that carry an image or a video.get_profile_tweets(..., include_replies=True)— the timeline with the replies of the profile included.refresh_manifest()and the CLI commandscweet refresh-manifest— read fresh GraphQL query ids from the live bundles of X at any moment, and report each id that rotated. A stale id answers 404 for every request, so call this when requests start to fail.- Every new method has an async twin and a CLI subcommand:
tweet-info,tweet-replies,reposters,search-users,trending,profile-media,profile-tweets --include-replies,user-info --ids.
Changed¶
- The query-id scrape now reaches the operations that
main.jsdoes not carry. Some operations live in lazy chunk files. The scrape reads the chunk maps that the page of X embeds, sweeps the likely chunks first, and stops at the find — measured: the missing id was found within 9 small fetches. Before, an id outsidemain.jscould never heal. - The bundled default ids were refreshed from the live bundle on 2026-09-13, and each new endpoint ships with its measured request allowance.
[5.6.0] - 2026-09-13¶
Every change below is additive. A script written against 5.5.x reads the same values from the same fields.
Fixed¶
- A profile reported 0 followers, 0 following and 0 tweets. X stopped sending the flat
legacyobject of a user and moved the counts intorelationship_counts,tweet_countsandaction_counts. The parser read only the old place, soget_user_info,get_followers,get_followingandget_verified_followersreturned a zero for every count. Captured 2026-09-13: one profile with 241,659,598 followers reported 0. The parser now reads the old place first and the new nodes after it, so an older answer of X keeps working. A real zero stays a zero. - A profile record now carries its banner image again, and the links inside the bio, for the same reason.
Added¶
get_verified_followers()/aget_verified_followers()and the CLI commandverified-followers. The engine already supported the endpoint of X for the verified followers of a profile, and no public method reached it.- A tweet record carries the fields that X already sends:
views,quotes,bookmarks,lang,hashtags,mentions,urls,in_reply_to_tweet_id,in_reply_to_user,is_quoteandis_retweet. No extra request: the data was in therawpayload of every row. quoted_tweetandretweeted_tweethold the embedded tweet as a full tweet record, one level deep.embedded_textkeeps its old value, which is the truncated text that X sends, so it can be shorter than thetextof the nested record.media.video_linksholds the highest-bitrate MP4 of each video. Anm3u8playlist is a stream index and not a file to save, so it is left out.- A user record carries
identity_verified,pinned_tweet_idsanddescription_urls.
Changed¶
- A count that X does not send reads
Noneinstead of0, so a caller can tell "unknown" from "zero". The countslikes,retweetsandcommentskeep their old default of0.
[5.5.1] - 2026-09-13¶
Fixed¶
beautifulsoup4is now a declared dependency. Two modules importbs4, and the package arrived only through a transitive dependency, so one dependency-resolver change could break a fresh install.- The license badge in the README opens the tracked license file. The old link answered 404.
- The FAQ names the release of the last live verification instead of a fixed month.
Changed¶
- The test dependencies are declared in
requirements-dev.txt. Before, a contributor who installed the package and ranpytestsaw 5 async tests fail, becausepytest-asynciowas installed only inside CI.pip install -e . -r requirements-dev.txtnow installs everything the suite needs. - The repository no longer tracks IDE settings.
[5.5.0] - 2026-09-09¶
A search works again. Released 5.4.0 returned HTTP 404 for every search, from every account, because X changed the page that the library reads to bootstrap itself. This release repairs that, and it raises the defaults that made a first run smaller and slower than the accounts allow. Every change below is verified with a live run against real accounts.
Fixed¶
- A search works again. Upgrade from 5.4.0 and 5.3: in those versions every search returned HTTP 404 from every account, and the library could not repair itself. Both bootstrap paths read
https://x.com, which answers a short shell page: it holds nomain.jsreference and no"ondemand.s"marker. So the manifest scrape found no bundle and kept a stale query ID, and the transaction-ID bootstrap built nox-client-transaction-idheader. X answers 404 when that header is absent. Both paths now readhttps://x.com/home, and the six bundled query IDs are refreshed. The failure looks like a dead account, and it is not: a 404 from every account describes the request, not the credentials. - A proxy URL that carries a
{session}placeholder works on every path. The proxy check on lease, the transaction-ID bootstrap, and the cookie bootstrap from anauth_tokeneach sent the literal text{session}as the proxy user name, and a provider answers HTTP 407 for that name. Only the account session builder replaced it. - A locked account is no longer read as a successful empty page. X locks an account behind a human challenge and answers HTTP 200 with code 326 in the body. The engine now maps that answer to a
lockedstatus, rests the account forlocked_cooldown_s(default 1 hour), and logs the unlock page (https://x.com/account/access). Before, the locked account stayed in rotation and silently returned nothing.
Changed¶
- The daily caps per account rose:
daily_requests_limitfrom 30 to 300,daily_tweets_limitfrom 600 to 6,000. The window limiter still bounds the burst rate to X's measured allowance (50 requests per 15 minutes). api_http_impersonatenow defaults to"chrome", which follows the newest Chrome fingerprint that the installedcurl_cffisupports. Before, the engine pinnedchrome120(a December 2023 browser), and an old TLS fingerprint next to current cookies is a mismatch that anti-bot systems weigh.min_delay_snow defaults to1.0: a floor between two requests of one account, so a burst does not arrive at wire speed. X counts the window total, so the floor costs little. Set0to remove it.- The followers/following paths now hold their own window budget,
relationship_window_request_limit(default 45). X allows 50 requests per window at the graph endpoint and restricts an account there more easily than at a search, so the budget keeps a margin of 5 instead of spending the full search budget. search()sorts by"Latest"by default."Top"is a ranked selection: a measured 20,000-tweet Top order returned about 1,900 unique tweets because the ranked pages repeat."Latest"is chronological and fills a volume order. Passdisplay_type="Top"for the old behavior. Note: a resume checkpoint keys on the full query includingdisplay_type, so a resume started under the old default does not match a run under the new default.
[5.4.0] - 2026-09-07¶
Added¶
- A proxy URL can now contain a
{session}placeholder, for examplehttp://user,session-{session}:pass@proxy.example.com:8000. Each account session replaces it with a unique token, so each account keeps its own exit IP on a rotating proxy. Without it, every account shares one exit IP, and when that exit IP dies, every retry reaches the same dead exit and the run loses data. With it, a session that is built again after a transport failure gets a new token and a new exit IP, so a retry recovers. A proxy without the placeholder works unchanged.
Changed¶
- Rate limiting now paces to X's real window (about 50 requests per account per 15 minutes) instead of a fixed delay between requests. A short run bursts its window budget and finishes far faster, and a long run no longer exceeds the window and triggers
429s that get accounts blocked. New config:window_request_limit(50) andrate_limit_window_s(900).min_delay_snow defaults to0(an optional floor);requests_per_minis deprecated.
Fixed¶
- An account now hands off a margin above its rate-limit window, so a large run meets far fewer
429s. X returnsx-rate-limit-remainingon each page, and the count can lag, so X sometimes answers429one request before the header reaches zero. A429loses the page and forces a retry. The run now stops an account when the header falls torate_limit_min_remaining(2 by default), rests it until its window resets (fromx-rate-limit-reset), and continues its cursor on a fresh account. A measured live 20,000-tweet order lost about 10% of the data to429s that the margin prevents. Setrate_limit_min_remaining=0for the old behaviour, which hands off only when the window is fully spent. - A large
Latestsearch over a wide time range now returns far more of the data. A cursor chain of X stops at a depth limit while tweets remain in the range, and the run used to end there and report success. A measured live order of 20,000 tweets over 3 months returned about 21% before and about 90% after. The run now continues the range from the time of the oldest tweet it saw, which re-queries only the older, unfetched part. The run also splits the range into at least one interval per account, so every account does work and no account uses up its request budget on one dense interval. ATopsearch is ranked by engagement, not by time, and the top tweets of a range and of each half are mostly the same tweets, so the run does not continue aTopinterval; use theLatestsort for a large volume. New config:max_interval_depth(100) caps the continuation as a safety backstop; the floorscheduler_min_interval_sand the result limit are the real bounds. Setmax_interval_depth=0to turn the continuation off. max_empty_pagesnow defaults to3instead of1. X sends an empty page in the middle of a chain while results remain, so a value of1ended the search at the first gap and returned a small part of the data while reporting success. A fixture where empty pages fall singly between data pages collected 2 of 6 tweets at1and all 6 at3. Set it back to1for the old behaviour.- An account is no longer blocked for 30 days by a routine message from X. A
401or403from a page of tweets is not proof that an account is dead — X sends it for a tweet one account cannot read, while the credentials still work. The worker now confirms with a self-lookup of the account's own handle before the long block. An unconfirmed error gives a short cooldown, so a healthy account returns in minutes instead of a month. Setpool_wait_max_sand the cooldown fields inScweetConfigto tune the behaviour. - A run no longer fails at once when every account is on a cooldown. It waits up to
pool_wait_max_s(120s by default) for a cooldown to expire and retries, because a cooldown expires. Setpool_wait_max_s=0to keep the old immediate failure. - Account repair no longer reports success when the write fails.
_attempt_account_repairreturnedTrueand logged "repair succeeded" even when the database write raised, so an operator believed the pool held a repaired account that was not there. It now returnsFalseand logs a warning. A failed account release also logs a warning instead of passing silently, because a failed release drops the account from the pool with no record.
[5.3.1] - 2026-05-19¶
Fixed¶
- Transaction-id bootstrap failure — Twitter changed the webpack manifest format, breaking the upstream
get_ondemand_file_urlregex (XClientTransaction1.0.2). Replaced with positional string extraction that survives manifest reshuffles.
[5.3.0] - 2026-04-14¶
Added¶
manifest_scrape_on_init=constructor shorthand — passmanifest_scrape_on_init=Truedirectly toScweet()without needingScweetConfig. Overrides the config value (same pattern asproxy=).--manifest-scrape-on-initCLI flag — scrape fresh GraphQL query IDs from X'smain.jsbundle on startup from the command line.save_name=parameter onsearch,get_profile_tweets,get_followers,get_following, andget_user_info— set a custom base filename for saved output (without extension).
Changed¶
- Documentation now marks
ct0as optional in cookies examples — Scweet bootstraps it automatically fromauth_token. - Manifest scrape docs updated to reflect the new top-level constructor arg and CLI flag.
[5.2.0] - 2026-04-03¶
Added¶
proxy=constructor shorthand — pass a proxy URL directly toScweet(proxy="http://host:port")without needingScweetConfig. Overrides any proxy set in config.
Fixed¶
- Python 3.9 compatibility — replaced
dict[str, Any] | strunion syntax withUnion[dict[str, Any], str]inScweetConfig. Pydantic evaluates field annotations at runtime, so the|syntax (PEP 604, Python 3.10+) caused aTypeErroron 3.9.
[5.1.0] - 2026-03-25¶
Added¶
- Command-line interface —
scweetcommand installed with the package. Subcommands:search,profile-tweets,followers,following,user-info. Full filter, auth, and output options. Use--prettyto print to stdout or--saveto write files. embedded_textfield — populated for quote tweets (quoted tweet text) and retweets (full retweet text), extracted from the GraphQL response.RateLimitError/AuthError— targeted exception subclasses ofRunFailed, raised automatically based on dominant HTTP error codes (429 →RateLimitError, 401/403 →AuthError). Both exported from the top-level package.- Canonical CSV column order —
TWEET_COLUMN_ORDER/USER_COLUMN_ORDERconstants ensure consistent, human-readable column ordering. User and media fields are flattened touser_screen_name,user_name,image_linksin CSV; Python/JSON output preserves nested structure. - Progress logging — human-readable start message with query/dates/limit; per-batch "Collected N / limit" updates; internal noise demoted to DEBUG.
- Output schema tables, method docstrings, troubleshooting guide, and full error hierarchy in
DOCUMENTATION.md.
Fixed¶
strictremoved fromScweetConfig— errors always raise exceptions (was silently swallowed by default).--tweet-typeCLI choices corrected tooriginals-only,replies-only,retweets-only,exclude-replies,exclude-retweets.--hashtagrenamed to--hashtags-any,--excluderenamed to--exclude-wordsto match library parameter names.- Default search window extended from 7 to 30 days when
sinceis omitted. configure_loggingremoved from public exports (still used internally by the CLI).AccountPoolExhaustedmessage now includes account counts (total, unusable, cooling down).db_pathconstructor argument now always takes precedence overScweetConfig(db_path=...).- Fixed
query=''in runner logs (was reading wrong field fromSearchRequest).
[5.0.0] - 2025¶
Why the rewrite? The migration from browser automation to direct GraphQL API calls began in v3. Twitter/X progressively restricted anonymous access from 2023 onwards, making the browser-based approach increasingly brittle. v5 completed the transition by removing the browser dependency entirely. Scweet now calls X's internal GraphQL API directly — the same one the web app uses — authenticated with browser cookies. The result is faster, leaner (no headless browser), and maintainable as X's API evolves.
Added¶
- Simplified public API:
Scweet,ScweetConfig,ScweetDB,configure_logging. - Flat
ScweetConfig(~35 fields, all with sensible defaults). No nested config sections. - Structured search filters:
all_words,any_words,exact_phrases,from_users,to_users,mentioning_users,hashtags_any,has_images,has_videos,min_likes,min_replies,min_retweets, geo filters, and more. - Profile tweets:
get_profile_tweets()/aget_profile_tweets(). - Followers/following:
get_followers()/aget_followers(),get_following()/aget_following()with optionalraw_jsontoggle. - User info:
get_user_info()/aget_user_info(). - Output saving:
save=True+save_format="csv|json|both"on all methods. - Resume interrupted searches:
resume=True(SQLite cursor checkpoints). - Auto-updating GraphQL query IDs:
ScweetConfig(manifest_scrape_on_init=True). - Account management via
ScweetDB:accounts_summary,list_accounts,repair_account,reset_account_cooldowns,clear_leases,reset_daily_counters, and more. - SQLite-backed account pool with lease/heartbeat lifecycle, daily caps, and cooldown tracking.
XClientTransactionheader generation (required by X since late 2024).configure_logging()helper withsimpleanddetailedprofiles.
Breaking (vs v4)¶
- Removed legacy import path
from Scweet.scweet import Scweet. - Removed legacy constructor args
n_splits,concurrency(on constructor — useScweetConfig(concurrency=...)). - Removed legacy query input keys on
scrape/ascrape(words,from_account, etc.). - Removed
ResumeMode,BootstrapStrategy,ApiHttpModefrom public exports. - Removed nested config sections (
pool.concurrency->concurrency,operations.daily_requests_limit->daily_requests_limit, etc.). - Config is now a flat
ScweetConfigPydantic model (nofrom_sourcesclass method). - Constructor is now
Scweet(cookies_file=, auth_token=, cookies=, db_path=, config=). scrape()/ascrape()replaced bysearch()/asearch().- Resume mode is
db_cursoronly (CSV-based resume removed).
Removed¶
nodriverdependency and browser-based login.requestsHTTP fallback (v5 requirescurl_cffi).- Legacy CSV resume mode.
DOCUMENTATION.md(was stale v4 docs; recreated for v5).