🎉 Premium Proxies · 3-Day Free TrialClaim Now →
Guides

How to Scrape Twitter/X Data (Public Data Only)

Scrape Twitter data without getting blocked: pull public tweets, profiles, and search via the X API or the no-login syndication endpoint, and dodge legal traps.

S SparkProxy 4 17 min read
Share
How to Scrape Twitter/X Data (Public Data Only)

Scrape Twitter data in 2026 and the first thing you learn is that most tutorials online are broken. The tools people still recommend (snscrape, twint, the surviving Nitter mirrors) died when X locked down logged-out access and throttled its guest API. What still works is narrower, and it splits into three routes depending on whether you want one public tweet, a profile, or search results. This guide walks all three, leads with the path X actually sanctions, and shows the one no-login endpoint that still returns public tweet JSON without an account or an API key.

Ethics and law come first

X is not a product catalog. Almost every field you can pull describes a real person and their speech, so the ethics carry more weight than on a price-scraping job, and they shape the design before any code gets written.

Three separate questions matter, and "it's public" only answers the first:

  • Access. In the US, scraping data that's publicly accessible with no login has generally not been treated as a Computer Fraud and Abuse Act violation. That is about access, not a license to do anything you want with what you collect.
  • Contract. X's Terms of Service prohibit automated collection outright. Scraping public pages can still breach that contract even where it isn't a computer-crime issue, and X has taken scrapers to court. Two different questions.
  • Data protection. A handle, a photo, and a post are personal data under GDPR and similar laws, and public availability is not an exemption. This one gets its own treatment near the end.

Guardrails that keep an X project defensible:

  • Collect public data only. No logged-in scraping of other people's timelines, no private or protected accounts, ever.
  • Pull the minimum you need. Aggregate counts and public post text, not dossiers on named individuals.
  • Rate-limit yourself and back off on errors so you never degrade the service for real users.
  • Honor deletion. If a tweet or account disappears, drop it from your store.
  • Talk to a lawyer before anything commercial. This is engineering guidance, not legal advice.

The legitimate reasons to want public X data are real: brand monitoring, trend research, crisis tracking, academic study. If that is your use case, the business-side patterns live in Using Proxies for Social Media Monitoring. The point of this section is that the use case has to survive scrutiny before the pipeline is worth building.

What changed: X locked the logged-out door

If you follow a 2021-era tutorial, it will fail, and understanding why saves you days.

For years, Twitter let anyone read tweets, profiles, and search results without an account. A whole ecosystem grew on top of that open access: snscrape parsed the logged-out search endpoints, Nitter proxied the site through guest tokens, and dozens of libraries scraped profile timelines with no auth at all.

That era ended in stages. In mid-2023 X briefly forced a login to view any tweet, reverted it days later after backlash (see TechCrunch's report), then settled into a durable middle ground: individual tweet pages sometimes render logged-out, but profile timelines and search now demand a login, and the guest-token flow that powered the old tools was throttled into uselessness through 2023 and 2024. Nitter instances went dark one after another for the same reason.

So the modern reality is blunt. There is no reliable, no-login way to scrape an arbitrary profile's timeline or run a keyword search on x.com. Those need the official API or an authenticated session. A single public tweet is the one exception, and it has a clean route covered below.

Free trial

Scraping at scale? Skip the blocks.

Fast, unblockable datacentre proxies with unlimited bandwidth.

The sanctioned path: the X API v2 and 2026 pricing

Before you scrape anything, check whether the official API covers your need, because it is the only route X blesses and the only one that scales without a compliance cloud over it.

X repriced its API more than once since 2023, and the model changed again in February 2026, when new developers were moved to pay-per-use. Confirm the live numbers on developer.x.com before you budget, because X moves them.

PlanWho can sign up nowTypical costRead cap
Pay-per-useNew and migrated developers~$0.005 per post read, ~$0.015 per post created ($0.20 if it has a link)2,000,000 reads / month
Basic (legacy)Existing subscribers only$200 / month~10,000 reads / month
Pro (legacy)Existing subscribers only$5,000 / month~1,000,000 reads / month
EnterpriseAnyone, custom contractFrom ~$42,000 / monthFull firehose

On June 1, 2026, X began auto-migrating remaining legacy Basic subscribers to pay-per-use. New signups can no longer buy Basic or Pro, so your practical choices today are pay-per-use or Enterprise.

The API base is https://api.x.com/2 (the old api.twitter.com host still resolves) and auth is a bearer token. Recent search covers roughly the last seven days:

import requests

BEARER = "YOUR_X_API_BEARER_TOKEN"

def search_recent(query, max_results=10):
    r = requests.get(
        "https://api.x.com/2/tweets/search/recent",
        headers={"Authorization": f"Bearer {BEARER}"},
        params={
            "query": query,                     # e.g. 'from:nasa -is:retweet'
            "max_results": max_results,         # 10 to 100 per page
            "tweet.fields": "created_at,public_metrics,lang",
            "expansions": "author_id",
            "user.fields": "username,verified,public_metrics",
        },
        timeout=30,
    )
    r.raise_for_status()
    return r.json()

Full-archive search (anything older than a week) is a Pro or Enterprise feature. Profile lookups are a separate endpoint that returns the public counts most projects actually want:

def user_by_username(username):
    r = requests.get(
        f"https://api.x.com/2/users/by/username/{username}",
        headers={"Authorization": f"Bearer {BEARER}"},
        params={"user.fields": "created_at,description,public_metrics,verified,location"},
        timeout=30,
    )
    r.raise_for_status()
    return r.json()

public_metrics on a user hands you followers_count, following_count, tweet_count, and listed_count in one call, with no HTML parsing and no proxy. If your need fits inside the API budget, stop here and use it. Everything below is for the public tweet data the API bills you for but a public endpoint gives away.

What public X data you can collect

Here is the reference set of public fields and where each one actually comes from, as of mid-2026.

FieldAvailable fromNotes
Tweet textSyndication endpoint, APIFull text, not truncated
Created atSyndication endpoint, APIISO 8601 timestamp
LanguageSyndication endpoint, API`lang`, e.g. `en`
Like countSyndication endpoint, API`favorite_count`
Reply countSyndication endpoint, API`conversation_count`
Retweet countAPI onlyNot in the syndication payload
Impression / view countAPI only (limited)Not public without auth
Author name and handleSyndication endpoint, API`user.name`, `user.screen_name`
Verified flagSyndication endpoint, API`is_blue_verified`
Hashtags, mentions, URLsSyndication endpoint, API`entities` object
Media (photo/video URLs)Syndication endpoint, API`mediaDetails`
Follower / following countsAPI only`public_metrics` on the user object
Profile bio and locationAPI only`description`, `location`

The split is the important part. Everything on a single tweet is reachable without login. Everything about a profile as a whole (followers, bio, timeline) needs the API. Plan your pipeline around that line instead of fighting it.

The no-login route for a single public tweet

Here is the piece most current tutorials miss. X's own embed system fetches public tweet data from a CDN endpoint that needs no login and no API key: https://cdn.syndication.twimg.com/tweet-result. It is what renders embedded tweets across the web, and it is what Vercel's open-source react-tweet library calls under the hood.

The endpoint wants a tweet ID and a token derived from that ID. The token is a lightweight plausibility check, not a signature, and react-tweet computes it like this:

// The scheme Vercel's react-tweet uses
const getToken = (id) =>
  ((Number(id) / 1e15) * Math.PI)
    .toString(6 ** 2)          // base 36
    .replace(/(0+|\.)/g, "");

A Python port that mirrors the same base-36 encoding:

import math, re

_DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz"

def _to_base36(value: float, frac_digits: int = 12) -> str:
    n = int(value)
    frac = value - n
    out = "0" if n == 0 else ""
    while n:
        out = _DIGITS[n % 36] + out
        n //= 36
    out += "."
    for _ in range(frac_digits):
        frac *= 36
        d = int(frac)
        out += _DIGITS[d]
        frac -= d
    return out

def tweet_token(tweet_id: str) -> str:
    raw = _to_base36((int(tweet_id) / 1e15) * math.pi)
    return re.sub(r"(0+|\.)", "", raw)

Fetch the tweet and treat an empty body as a miss (deleted, protected, or a token that needs recomputing). Fall back to the official oEmbed endpoint or the API if you get nothing back:

import requests

def fetch_tweet(tweet_id: str) -> dict | None:
    r = requests.get(
        "https://cdn.syndication.twimg.com/tweet-result",
        params={"id": tweet_id, "token": tweet_token(tweet_id), "lang": "en"},
        headers={"User-Agent": "Mozilla/5.0"},
        timeout=20,
    )
    if r.status_code != 200 or not r.text.strip():
        return None
    return r.json()

The JSON is rich. Flatten the parts you care about:

def parse_tweet(data: dict) -> dict:
    user = data.get("user", {})
    return {
        "id": data.get("id_str"),
        "text": data.get("text"),
        "created_at": data.get("created_at"),
        "likes": data.get("favorite_count"),
        "replies": data.get("conversation_count"),
        "lang": data.get("lang"),
        "author_name": user.get("name"),
        "author_handle": user.get("screen_name"),
        "author_verified": user.get("is_blue_verified"),
        "hashtags": [h["text"] for h in data.get("entities", {}).get("hashtags", [])],
        "media": [m.get("media_url_https") for m in data.get("mediaDetails", [])],
    }

One honest caveat: this payload carries likes (favorite_count) and replies (conversation_count) but not retweet or impression counts. Those live behind the authenticated API. Don't build a dashboard that assumes fields the embed route never returns.

Set up SparkProxy to scrape Twitter data

The syndication endpoint runs fine from your laptop for a dozen tweets. Push past that and cdn.syndication.twimg.com rate-limits per IP, so you start seeing empty responses and 429s. Routing the same request through the SparkProxy Scraping API puts a fresh IP behind each call and takes the rotation off your plate.

Because the endpoint returns plain JSON with no browser rendering needed, keep render_js=false, which bills at 1 credit per request instead of 5. Add premium_proxy=true to route through residential IPs that shrug off the per-IP limit. If you are unsure why datacenter IPs get flagged first here, What Is a Residential Proxy: Types and Use Cases explains the reputation difference.

import requests

SPARK_API = "https://scrape.sparkproxy.io/api/v1"
SPARK_KEY = "YOUR_API_KEY"

def fetch_tweet_via_spark(tweet_id: str) -> dict | None:
    target = (
        "https://cdn.syndication.twimg.com/tweet-result"
        f"?id={tweet_id}&token={tweet_token(tweet_id)}&lang=en"
    )
    r = requests.get(
        SPARK_API,
        headers={"X-API-Key": SPARK_KEY},
        params={
            "url": target,
            "render_js": "false",       # plain JSON endpoint, 1 credit
            "premium_proxy": "true",    # residential IPs dodge the per-IP limit
        },
        timeout=60,
    )
    if r.status_code != 200 or not r.text.strip():
        return None
    return r.json()

The base endpoint is https://scrape.sparkproxy.io/api/v1 and auth is a single header, X-API-Key. The full parameter list lives in the Scraping API docs. If you are weighing this against building your own rotation and IP pool, Web Scraping API vs Self-Managed Proxies lays out the build-versus-buy math.

Collect tweets at scale without tripping limits

With a fetcher that rotates IPs, batching is mostly discipline: retry with backoff, cache what you already have, and persist as you go so a crash at tweet 8,000 doesn't cost you the first 7,999.

import time, random

def collect(ids):
    out = {}
    for tid in ids:
        for attempt in range(3):
            data = fetch_tweet_via_spark(tid)
            if data and data.get("text"):
                out[tid] = parse_tweet(data)
                break
            time.sleep(2 ** attempt + random.random())   # backoff + jitter
    return out

Four habits keep a Twitter scraper healthy at volume:

  • Deduplicate by id_str. A tweet ID is stable, so a set of seen IDs stops you paying to fetch the same tweet twice.
  • Cache hard. A tweet's text never changes and its like count barely moves after a day. Re-scraping settled tweets is wasted spend.
  • Keep concurrency modest. Even through rotating IPs, a slower steady rate draws less attention than a burst.
  • Stamp every row with a scraped_at timestamp. It gives you clean time series and a record of exactly when each data point was true, which matters for the compliance trail.

For anti-block theory beyond this, How to Avoid Getting Your Proxy Blocked goes deep on the mechanics.

Profiles and search: your real options

The syndication route is per-tweet only. For a profile timeline or a keyword search, the honest answer is that the no-login web paths are gone. Here is the full decision matrix.

You wantBest routeLogin or keyNotes
One public tweet`cdn.syndication.twimg.com/tweet-result`NoneLikes and replies only, no retweet counts
Official embed HTML`publish.twitter.com/oembed`NoneReturns embed markup plus author name and URL
A profile's public fields`/2/users/by/username`X APIFollowers, bio, counts
A profile's tweet timeline`/2/users/:id/tweets`X APIPaginated
Keyword or hashtag search`/2/tweets/search/recent`X APILast ~7 days
Full historical search`/2/tweets/search/all`X API (Pro or Enterprise)Full archive

The official oEmbed endpoint is a clean, no-key way to grab a single tweet's embed markup and author details:

curl -G "https://publish.twitter.com/oembed" \
  --data-urlencode "url=https://x.com/nasa/status/1234567890123456789" \
  --data-urlencode "omit_script=true"

There is a fourth path, and it deserves a warning rather than a snippet. Some scrapers drive a logged-in browser session against x.com and read the internal GraphQL endpoints (UserByScreenName, UserTweets, SearchTimeline) using the account's bearer, guest token, and CSRF cookie. It works. It also means automating your own X account against Terms that prohibit scraping, which can get the account suspended and, at commercial scale, invite legal exposure. You can pass session cookies to the Scraping API through the cookies or forward_headers parameters, but weigh what you are risking first. For anything that has to survive scrutiny, the API or Enterprise access is the route that holds up.

What breaks naive Twitter scrapers

Copy-paste scrapers fail on X for four specific reasons. Knowing the signal tells you which route to take instead.

SignalWhat you seeWhat it means
Login wallA consent or sign-in modal on a profile or search pageLogged-out access to that surface is gated; use the API
Guest-token failureOld snscrape/Nitter tooling returns empty or errorsThe guest flow those tools used is throttled and unreliable
Fingerprint blockA raw HTTP client gets challenged or blank pagesX reads TLS (JA3/JA4) and header order; a non-browser client stands out
Per-IP rate limit429s or empty JSON after a burst of requestsYou are hammering one IP; rotate residential IPs and slow down

The takeaway is not "try harder to scrape the HTML." It is "match each need to the route that still works": syndication for single tweets, the API for profiles and search, and rotating residential IPs behind whichever endpoint you hit at volume.

Frequently asked questions

FAQ

It depends on jurisdiction and method. In the US, collecting public, logged-out data has generally not been treated as a CFAA violation, and in X Corp. v. Bright Data (N.D. Cal., 2024) a court dismissed X's claims over scraping public data. But X's Terms of Service still prohibit scraping, and under GDPR public personal data still needs a lawful basis. Stick to public data, avoid automating logged-in accounts, and get legal advice before commercial use.

Only for individual tweets. X gates profile timelines and search behind login, so logged-out page scraping of those surfaces is unreliable. A single public tweet is still reachable through the syndication endpoint (cdn.syndication.twimg.com/tweet-result) that powers embeds, which needs no account or API key.

From the syndication endpoint you get a public tweet's text, creation time, language, author name and handle, verified flag, hashtags, and media URLs, plus like and reply counts. Retweet counts and impression counts are not in that payload. For anything beyond a single tweet you need the X API.

Through the X API v2. Use /2/tweets/search/recent for keyword or hashtag search over the last seven days, and /2/users/by/username plus /2/users/:id/tweets for a profile and its timeline. Full-archive search needs Pro or Enterprise access. There is no reliable no-login route for search in 2026.

They relied on unauthenticated guest tokens and logged-out endpoints that X restricted through 2023 and 2024. Once guest access was throttled and the login wall tightened, those tools broke. The syndication embed endpoint and the official API are what still return public data.

X switched new developers to pay-per-use in February 2026: roughly $0.005 per post read and $0.015 per post created ($0.20 with a link), capped at two million reads a month. Legacy Basic ($200/month) and Pro ($5,000/month) are closed to new signups, and Enterprise starts around $42,000/month. Confirm current numbers on developer.x.com before budgeting.

Limited-time · 50% off

Get 50% off your first purchase

Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.

Offer ends soon — claim it before it's gone

Claim Discount

About the Author

SparkProxy Technical Team. The SparkProxy engineering team builds and runs global datacenter and residential proxy infrastructure plus a managed Scraping API. This guide reflects patterns tested against x.com endpoints in 2026 using Python 3.11+ and requests 2.32+. X changes its access rules, API pricing, and internal endpoints often, so treat the token scheme, field list, and prices as a starting point and re-check them before a large run. This article is engineering guidance on public data, not legal advice.

Sources: TechCrunch on X's login requirement, July 2023 · X API documentation (developer.x.com) · X Terms of Service · Vercel react-tweet (syndication endpoint reference) · SparkProxy Scraping API docs

Keep reading

Related articles