A tested walkthrough for Twitter/X Posts -- the right proxy type, 77% tested success rate, working code, and what to avoid to stay compliant.
A Twitter/X scraper pulls tweet text, media links, and engagement counts without logging in, reading the public syndication endpoint through residential rotating proxies. X enforces a login wall, tight per-IP rate limits, and Arkose Labs challenges on most surfaces, and KnoxProxy residential IPs held a 77% success rate on the syndication endpoint during the last test cycle.
| Anti-bot system | Login wall + rate limiting + Arkose Labs bot detection |
| Challenge types | Login wall for most timeline, search, and follower-list views, Arkose Labs FunCaptcha on flagged sessions, Aggressive per-IP and per-account rate limits, Guest-token validation on public endpoints, Follower and following lists gated behind an authenticated session since 2023 |
| Rate limit behavior | Since 2023, X requires a login for most browsing, and even the syndication endpoints used for embedding single posts and profile timelines apply tight per-IP rate limits. Exceeding a modest request rate returns an HTTP 429 or a rendered rate-limit page, and authenticated API rate limits apply separately per account or app key regardless of proxy rotation. |
| Tested success rate | 77% |
"""
KnoxProxy python twitter scraper for X (Twitter) posts and public profile
timelines. Uses the public syndication endpoints designed for embedding
tweets and profile widgets, neither of which requires a logged-in session.
"""
import random
import time
import requests
PROXY_HOST = "proxy.knoxproxy.com"
PROXY_PORT = 10000 # residential rotating
PROXY_USER = "your_username"
PROXY_PASS = "your_password"
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
),
"Accept": "application/json",
}
def proxy_dict() -> dict:
proxy_url = f"http://{PROXY_USER}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}"
return {"http": proxy_url, "https": proxy_url}
def fetch_tweet(tweet_id: str, max_retries: int = 3) -> dict:
"""Scrape a single tweet by ID from the public syndication endpoint."""
url = "https://cdn.syndication.twimg.com/tweet-result"
params = {"id": tweet_id, "lang": "en"}
for attempt in range(1, max_retries + 1):
try:
resp = requests.get(url, headers=HEADERS, params=params, proxies=proxy_dict(), timeout=20)
if resp.status_code == 200:
return parse_tweet(resp.json(), tweet_id)
if resp.status_code == 429:
print(f"Attempt {attempt}: rate limited, rotating IP before retry")
else:
print(f"Attempt {attempt}: X returned status {resp.status_code}")
except requests.exceptions.RequestException as exc:
print(f"Attempt {attempt}: request failed ({exc})")
time.sleep(random.uniform(4, 8))
raise RuntimeError(f"Failed to fetch tweet {tweet_id} after {max_retries} attempts")
def parse_tweet(payload: dict, tweet_id: str) -> dict:
photos = payload.get("photos", [])
video = payload.get("video")
return {
"tweet_id": tweet_id,
"text": payload.get("text"),
"author": payload.get("user", {}).get("screen_name"),
"like_count": payload.get("favorite_count"),
"retweet_count": payload.get("conversation_count"),
"media_urls": [p.get("url") for p in photos] + ([video.get("poster")] if video else []),
}
def fetch_profile_timeline(screen_name: str, max_retries: int = 3) -> list[dict]:
"""Pull a rolling window of recent public posts and media for one profile.
This is the same widget feed X serves to power embedded timelines on
external sites, so it stays reachable without a login while X's own
timeline and search pages sit behind the authentication wall. This is
the core of a twitter media scraper: photos and a video poster travel
in the same payload as the post text.
"""
url = "https://cdn.syndication.twimg.com/timeline/profile"
params = {"screen_name": screen_name, "lang": "en"}
for attempt in range(1, max_retries + 1):
try:
resp = requests.get(url, headers=HEADERS, params=params, proxies=proxy_dict(), timeout=20)
if resp.status_code == 200:
body = resp.json()
items = body.get("body", {}).get("items", [])
# NOTE: item keys mirror the tweet-result payload above;
# re-verify against a live profile if X changes the widget shape.
return [parse_tweet(item, item.get("id_str", "")) for item in items]
if resp.status_code == 429:
print(f"Attempt {attempt}: rate limited on profile timeline, rotating IP")
else:
print(f"Attempt {attempt}: X returned status {resp.status_code}")
except requests.exceptions.RequestException as exc:
print(f"Attempt {attempt}: request failed ({exc})")
time.sleep(random.uniform(4, 8))
raise RuntimeError(f"Failed to fetch profile timeline for {screen_name} after {max_retries} attempts")
if __name__ == "__main__":
print(fetch_tweet("1743456789012345678"))
time.sleep(random.uniform(4, 8))
for tweet in fetch_profile_timeline("examplehandle"):
print(tweet)
time.sleep(random.uniform(4, 8))
"""
Append parsed KnoxProxy twitter scraper output to a CSV file for analysis,
skipping tweet IDs that have already been stored. Built for the fetch
functions in twitter_x_scraper.py.
"""
import csv
import os
FIELDNAMES = ["tweet_id", "text", "author", "like_count", "retweet_count", "media_urls"]
def load_existing_ids(path: str) -> set[str]:
if not os.path.exists(path):
return set()
with open(path, newline="", encoding="utf-8") as f:
return {row["tweet_id"] for row in csv.DictReader(f)}
def append_tweets(path: str, tweets: list[dict]) -> int:
existing = load_existing_ids(path)
new_rows = [t for t in tweets if t["tweet_id"] not in existing]
write_header = not os.path.exists(path)
with open(path, "a", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=FIELDNAMES)
if write_header:
writer.writeheader()
for row in new_rows:
row = dict(row)
row["media_urls"] = ";".join(row.get("media_urls") or [])
writer.writerow(row)
return len(new_rows)
if __name__ == "__main__":
from twitter_x_scraper import fetch_profile_timeline
tweets = fetch_profile_timeline("examplehandle")
added = append_tweets("tweets.csv", tweets)
print(f"Added {added} new rows to tweets.csv")
{
"tweet_id": "1743456789012345678",
"text": "shipping a small update to the dashboard today",
"author": "examplehandle",
"like_count": 1420,
"retweet_count": 96,
"media_urls": []
}
Added 4 new rows to tweets.csvInstall requests for calling X's public syndication endpoints; this twitter scraper python setup needs no browser automation or authentication library.
pip install requestsRoute requests through the KnoxProxy residential rotating gateway to cut down on 429 responses during twitter scraping.
PROXY_HOST = "proxy.knoxproxy.com"
PROXY_PORT = 10000
PROXY_USER = "your_username"
PROXY_PASS = "your_password"Call the syndication endpoint with a post ID to retrieve text, media links, and engagement counts without logging in.
Request the syndication widget feed for a screen name to pull a rolling window of recent public tweets and media, the same feed X uses to power embedded timeline widgets on external sites.
Back off and rotate to a fresh IP whenever a rate-limit response comes back instead of retrying immediately. Authenticated requests draw from a separate account-level quota that IP rotation alone cannot get around.
Write parsed tweet and media records to CSV so engagement metrics can be tracked over time without re-fetching the same posts.
Twitter/X Posts runs login wall + rate limiting + Arkose Labs bot detection. The tested setup is residential rotating proxies, targeting no specific geo requirement unless testing region-locked content, with this rotation: Rotate IP on every request. That combination held a 77% success rate on the Jun 30, 2026 test run.
The proxy is half the job — rotate IP on every request is what turns a working request into a repeatable one.
Rotate IP on every request.
No specific geo requirement unless testing region-locked content.
Twitter/X Posts leans on login wall for most timeline, search, and follower-list views. Since 2023, X requires a login for most browsing, and even the syndication endpoints used for embedding single posts and profile timelines apply tight per-IP rate limits. Exceeding a modest request rate returns an HTTP 429 or a rendered rate-limit page, and authenticated API rate limits apply separately per account or app key regardless of proxy rotation.
Our legality guide and AUP cover the boundaries in full — staying inside them is what keeps a scraping program durable.
A limited amount of data is available without an account through the public syndication endpoints used to scrape tweets and embed profile timeline widgets. Most timeline, search, and profile browsing now requires a login, which sits outside anonymous public-data collection.
Residential rotating proxies work best for web scraping twitter data, since X's rate limiting applies aggressively per IP and datacenter ranges hit 429 responses noticeably faster. Rotate on every request rather than every few requests, and swap out any IP the moment it returns a 429.
X introduced a login requirement for most browsing in 2023 and tightened rate limits significantly, closing off much of what was previously reachable through anonymous HTML scraping. Twitter scraping today leans on the narrower syndication endpoints instead, and only for public, non-protected accounts.
Yes. The syndication endpoint response includes photo URLs and a video poster image alongside the tweet text, so a twitter media scraper can pull all of it in the same request used for text and engagement counts, without a separate media-only call.
You can't, through this anonymous approach. Follower and following lists have required a logged-in session since 2023, so no amount of proxy rotation reaches that data anonymously. Reaching follower counts at all needs an authenticated session paced well within X's account-level limits.
Yes. Authenticated requests draw from a quota tied to the account or API key, which resets on its own window regardless of proxy or IP. Anonymous syndication endpoint requests are limited per IP instead, which is what proxy rotation actually helps with.
For public posts without login, the syndication endpoint above is the fastest option since it returns JSON directly and skips rendering a full page or running a headless browser. That is lighter than any approach that needs an authenticated session.
It works for posts and profile timelines belonging to public, non-protected accounts only. Posts from protected accounts, deleted posts, or suspended profiles return an error or an empty result no matter how good the proxy or how carefully requests are paced.
The Python code above is a tested twitter scraper for both single posts and profile timelines, with proxy rotation, retries, and CSV storage already wired in. Run it as-is or extend the parsing, though timeline and search browsing in a full session still need a login regardless of tooling.
residential proxies -- 77% tested success, instant activation, 14-day money-back guarantee.