How to Download All Tweets and Replies From an X User
Downloading a user's X history is not just "ask for a big number and hope the response finishes." Large exports need a cursor loop, a place to save raw results, and a small amount of bookkeeping so retries do not create duplicate rows.
This guide uses TwexAPI's paginated endpoint, POST /twitter/tweets-replies/page, to export a user's own posts and replies one page at a time. It is a better default for archives, research, compliance review, and repeatable data jobs than a single count-based request.
Export Strategy
The safest export has two files:
- Raw JSONL: one tweet object per line, kept as the source of truth.
- CSV projection: a smaller table for spreadsheets, BI tools, or annotation.
Keep both. The CSV is easier to review, but the raw JSON keeps fields you may need later, such as media, quotes, reply metadata, or author verification fields.
API Endpoint
POST https://api.twexapi.io/twitter/tweets-replies/page
Authorization: Bearer <your_token>
Content-Type: application/jsonRequest body:
| Field | Required | Description |
|---|---|---|
screen_name | Yes | Target X username, with or without @ |
next_cursor | No | Cursor returned by the previous page |
Each response includes the current data array plus pagination metadata:
| Field | Meaning |
|---|---|
data | Current page of tweets and replies |
has_next_page | Whether another page is available |
next_cursor | Cursor to pass into the next request |
The first request does not include next_cursor:
curl --request POST \
--url https://api.twexapi.io/twitter/tweets-replies/page \
--header 'Authorization: Bearer <your_token>' \
--header 'Content-Type: application/json' \
--data '{
"screen_name": "elonmusk"
}'To continue, send the returned cursor:
{
"screen_name": "elonmusk",
"next_cursor": "1234567890"
}Python Cursor Export
The script below writes raw pages to tweets-replies.jsonl. It deduplicates by tweet_id, stops when has_next_page is false, and keeps the cursor loop explicit so it is easy to resume or debug.
1import json
2import os
3import time
4
5import requests
6
7API_URL = "https://api.twexapi.io/twitter/tweets-replies/page"
8TOKEN = os.environ["TWEXAPI_TOKEN"]
9SCREEN_NAME = "elonmusk"
10OUTPUT_PATH = "tweets-replies.jsonl"
11
12def fetch_page(next_cursor=None):
13 body = {"screen_name": SCREEN_NAME}
14 if next_cursor:
15 body["next_cursor"] = next_cursor
16
17 response = requests.post(
18 API_URL,
19 headers={
20 "Authorization": f"Bearer {TOKEN}",
21 "Content-Type": "application/json",
22 },
23 json=body,
24 timeout=45,
25 )
26 response.raise_for_status()
27 payload = response.json()
28 if payload.get("code") not in (None, 200):
29 raise RuntimeError(payload.get("msg", "TwexAPI request failed"))
30 return payload
31
32seen_ids = set()
33next_cursor = None
34page_count = 0
35saved_count = 0
36
37with open(OUTPUT_PATH, "w", encoding="utf-8") as output:
38 while True:
39 payload = fetch_page(next_cursor)
40 page_count += 1
41
42 items = payload.get("data") or []
43 for tweet in items:
44 if not tweet:
45 continue
46
47 tweet_id = tweet.get("tweet_id") or tweet.get("id")
48 if not tweet_id or tweet_id in seen_ids:
49 continue
50
51 seen_ids.add(tweet_id)
52 output.write(json.dumps(tweet, ensure_ascii=False) + "\n")
53 saved_count += 1
54
55 print(f"page={page_count} fetched={len(items)} saved={saved_count}")
56
57 if not payload.get("has_next_page") or not payload.get("next_cursor"):
58 break
59
60 next_cursor = payload["next_cursor"]
61 time.sleep(0.5)
62
63print(f"Finished: {saved_count} unique tweets and replies saved to {OUTPUT_PATH}")Run it with:
TWEXAPI_TOKEN="your_twexapi_bearer_token" python export-tweets-replies.pyCreate A CSV Projection
After saving raw JSONL, create a smaller CSV with the fields your team actually reviews.
1import csv
2import json
3
4INPUT_PATH = "tweets-replies.jsonl"
5OUTPUT_PATH = "tweets-replies.csv"
6
7fields = [
8 "tweet_id",
9 "created_at_datetime",
10 "text",
11 "lang",
12 "favorite_count",
13 "retweet_count",
14 "reply_count",
15 "quote_count",
16 "view_count",
17 "in_reply_to",
18 "in_reply_to_screen_name",
19 "is_quote_status",
20]
21
22with open(INPUT_PATH, encoding="utf-8") as source, open(
23 OUTPUT_PATH, "w", encoding="utf-8", newline=""
24) as target:
25 writer = csv.DictWriter(target, fieldnames=fields)
26 writer.writeheader()
27
28 for line in source:
29 tweet = json.loads(line)
30 row = {field: tweet.get(field) for field in fields}
31 row["tweet_id"] = tweet.get("tweet_id") or tweet.get("id")
32 row["text"] = tweet.get("full_text") or tweet.get("text") or ""
33 writer.writerow(row)
34
35print(f"Wrote {OUTPUT_PATH}")Fields To Preserve
For research or audit work, do not keep only text. Store enough context to explain the row later.
| Field | Why it matters |
|---|---|
tweet_id | Stable deduplication and citation key |
created_at / created_at_datetime | Timeline ordering and time-window analysis |
text / full_text | Human-readable content |
in_reply_to, in_reply_to_screen_name | Separates replies from standalone posts |
favorite_count, retweet_count, reply_count, quote_count | Engagement review |
media, urls, hashtags, cashtags | Content enrichment |
user | Author snapshot at export time |
Production Notes
- Save each page as it arrives. Do not wait until the full export finishes.
- Store the last successful
next_cursorif you need resumable jobs. - Deduplicate by
tweet_id; retries and cursor changes can otherwise create repeated rows. - Keep raw JSONL even when the CSV is all you need today.
- Record the export time, target
screen_name, and script version with the output files. - For recurring exports, compare new rows against previously saved
tweet_idvalues.
Count Endpoint Or Page Endpoint?
TwexAPI also exposes GET /twitter/{screen_name}/tweets-replies/{count}. It can be convenient for small one-off pulls where you know the target count.
For serious exports, use POST /twitter/tweets-replies/page. Cursor pagination makes progress visible, lowers the cost of a failed request, and gives you better control over storage and retries.