Answers

Twitter's free academic API is gone. How can researchers collect tweets now?

Short answer

X ended free academic access in 2023. Today a researcher can pay X directly ($0.005 per post through full-archive search, so 20,000 posts cost $100) or collect public posts with X post search at $0.006 per page of about 20, which puts 20,000 posts at $6.00. Slice queries by month and keep the exact queries for your methods section.

A PhD student studying how a policy debate unfolded on X needs about 20,000 posts from three months of 2026. Until 2023 the Academic Research track would have given her full-archive search for free. Twitter announced its end in February 2023, and a survey that autumn by the Coalition for Independent Technology Research found more than 100 studies cancelled, suspended or changed as a result. The data is still reachable; it now costs money, so plan the budget the way you would plan lab time.

Two routes and what they cost

Route 20,000 posts Notes
X full-archive search, pay-per-use $100 back to 2006, X's own operators, X developer account
X post search here $6.00 X search-box operators, about 20 posts per page

Account-level work uses X account posts ($0.006 per page) and X account profile ($0.001 per account), for example to record each author's follower count at collection time.

Collect month by month

import json, os, requests

API = "https://api.okdata.online/v1"
H = {"Authorization": f"Bearer {os.environ['OKDATA_API_KEY']}"}
BASE = '("carbon tax" OR #carbontax) lang:en -filter:replies'
MONTHS = [("2026-07-01", "2026-08-01"), ("2026-08-01", "2026-09-01"), ("2026-09-01", "2026-10-01")]

with open("corpus.jsonl", "a", encoding="utf-8") as out:
    for since, until in MONTHS:
        query, cursor = f"{BASE} since:{since} until:{until}", None
        for _ in range(350):  # page budget per month
            params = {"query": query, "sort": "latest", "view": "full", **({"cursor": cursor} if cursor else {})}
            body = requests.get(f"{API}/x/posts/search", headers=H, params=params, timeout=60).json()
            if body["code"] != "ok":
                raise RuntimeError(f"{body['code']}: {body['message']} ({body['request_id']})")
            for post in body["data"]["items"]:
                out.write(json.dumps({"query": query, "fetched_at": body["meta"]["fetched_at"], **post}, ensure_ascii=False) + "\n")
            if not body["page"]["has_more"]:
                break
            cursor = body["page"]["next_cursor"]

Storing the query and fetched_at with every record makes the dataset reproducible to the extent a live platform allows.

Method notes reviewers will ask about

  • The posts are what X search returns to the public for each query. That is neither a random sample nor a complete census. Say so.
  • Engagement numbers change after collection. Report when you collected them.
  • Deleted posts and protected accounts are not included, which can bias studies of contentious topics.
  • Follow your ethics board's rules on public data. Sharing post IDs with your paper, rather than full text, is the common practice.

Other doors worth knowing

In the EU, Article 40 of the Digital Services Act gives vetted researchers a legal route to request platform data, which suits large studies with institutional backing. X's official full-archive search is the cleanest choice when completeness and provenance matter most. Existing published datasets of post IDs can sometimes save collection altogether. For multi-platform designs, note that many researchers moved to Reddit, TikTok and YouTube; the trend research guide shows a cross-platform collection, and the pricing page lists every endpoint.

Updated