How to Fetch X Tweet Replies by Page with TwexAPI
Replies are where a post becomes a conversation. If you are measuring launch feedback, tracking community response, or preparing moderation review data, you need more than the root tweet. You need the replies, the order you requested them in, and the cursor trail that explains how the dataset was collected.
For new integrations, use TwexAPI's paginated replies endpoint: POST /twitter/tweets/{tweet_id}/replies/page. It returns replies page by page and avoids the timeout risk that comes with trying to pull a large thread in one request.
Endpoint and Sort Modes
Send a POST request to:
https://api.twexapi.io/twitter/tweets/<tweet_id>/replies/pageThe path parameter is the root tweet ID. The JSON body supports:
| Field | Required | How to use it |
|---|---|---|
sort_by | No | Recency, Relevance, or Likes. Use one sort mode for the whole export. |
next_cursor | No | Leave empty on the first request. Pass the previous response's next_cursor to continue. |
The first page can include root_tweet along with the first data page of replies. Later pages return more replies plus has_next_page and next_cursor.
Fetch the First Page
Use Recency when you want a timeline-style export. Use Relevance or Likes only when ranking is part of the analysis.
curl --request POST \
--url https://api.twexapi.io/twitter/tweets/<tweet_id>/replies/page \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{
"sort_by": "Recency"
}'A simplified response looks like this:
1{
2 "code": 200,
3 "msg": "success",
4 "root_tweet": {
5 "tweet_id": "1803006263529541838",
6 "text": "<root tweet text>"
7 },
8 "data": [
9 {
10 "tweet_id": "1234567890123456789",
11 "text": "<reply text>",
12 "created_at": "Mon Jul 01 12:34:56 +0000 2025",
13 "user": {
14 "screen_name": "example_user"
15 }
16 }
17 ],
18 "has_next_page": true,
19 "next_cursor": "cursor_from_this_response"
20}Continue with next_cursor
When has_next_page is true, pass the cursor into the next request. Keep the same tweet_id and sort_by throughout the export.
curl --request POST \
--url https://api.twexapi.io/twitter/tweets/<tweet_id>/replies/page \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{
"sort_by": "Recency",
"next_cursor": "cursor_from_this_response"
}'Changing the sort mode during pagination makes the result harder to explain. If you need a second ranking view, run a separate export and label it clearly.
Python Export Script
The script below writes replies to JSONL, saves the root tweet once, deduplicates by tweet_id, and stores page metadata for auditability.
1import json
2import time
3from datetime import datetime, timezone
4from pathlib import Path
5
6import requests
7
8TOKEN = "<your_bearer_token>"
9TWEET_ID = "<tweet_id>"
10SORT_BY = "Recency"
11URL = f"https://api.twexapi.io/twitter/tweets/{TWEET_ID}/replies/page"
12REPLIES_OUT = Path(f"{TWEET_ID}-replies.jsonl")
13ROOT_OUT = Path(f"{TWEET_ID}-root.json")
14META_OUT = Path(f"{TWEET_ID}-replies-meta.json")
15
16headers = {
17 "Authorization": f"Bearer {TOKEN}",
18 "Content-Type": "application/json",
19}
20
21cursor = None
22page_number = 0
23seen_reply_ids = set()
24page_log = []
25
26with REPLIES_OUT.open("w", encoding="utf-8") as replies_file:
27 while True:
28 payload = {"sort_by": SORT_BY}
29 if cursor:
30 payload["next_cursor"] = cursor
31
32 response = requests.post(URL, headers=headers, json=payload, timeout=30)
33 response.raise_for_status()
34 body = response.json()
35
36 page_number += 1
37
38 if page_number == 1 and body.get("root_tweet"):
39 ROOT_OUT.write_text(
40 json.dumps(body["root_tweet"], ensure_ascii=False, indent=2),
41 encoding="utf-8",
42 )
43
44 replies = body.get("data") or []
45 for reply in replies:
46 reply_id = reply.get("tweet_id") or reply.get("id")
47 if not reply_id or reply_id in seen_reply_ids:
48 continue
49 seen_reply_ids.add(reply_id)
50 replies_file.write(json.dumps(reply, ensure_ascii=False) + "\n")
51
52 page_log.append({
53 "page": page_number,
54 "items": len(replies),
55 "has_next_page": body.get("has_next_page"),
56 "next_cursor": body.get("next_cursor"),
57 })
58
59 if not body.get("has_next_page") or not body.get("next_cursor"):
60 break
61
62 cursor = body["next_cursor"]
63 time.sleep(1)
64
65META_OUT.write_text(json.dumps({
66 "tweet_id": TWEET_ID,
67 "sort_by": SORT_BY,
68 "exported_at": datetime.now(timezone.utc).isoformat(),
69 "unique_replies": len(seen_reply_ids),
70 "pages": page_log,
71}, ensure_ascii=False, indent=2), encoding="utf-8")
72
73print(f"Saved {len(seen_reply_ids)} replies to {REPLIES_OUT}")For scheduled jobs, persist the last successful cursor after each page. That makes failures recoverable without discarding the pages you already collected.
Fields Worth Keeping
At minimum, store:
- The raw reply object.
tweet_id,created_at, text fields, author fields, and engagement counts if present.in_reply_to,reply_to,conversation_id, or related thread fields when returned.- The root
tweet_id,sort_by, export time, page number, and cursor metadata. - A normalized reply URL such as
https://x.com/i/web/status/<reply_id>.
Keep raw data beside normalized rows. Reply schemas can evolve, and downstream analysis often needs fields you did not expect at export time.
Analysis Notes
Reply exports are useful for:
- Measuring early product or campaign feedback.
- Reviewing customer concerns after an announcement.
- Building moderation queues around a specific post.
- Studying how discussion changes when sorted by recency versus likes.
- Sampling replies for manual labeling before sentiment or topic modeling.
Do not treat reply data as a complete public opinion poll. Replies are shaped by ranking, deletion, account visibility, and who chooses to respond. Store enough context so the limits of the dataset are visible.
Common Pitfalls
- Pulling only the first page and calling it the full conversation.
- Changing
sort_byhalfway through pagination. - Dropping
root_tweet, then losing the context for the reply dataset. - Running sentiment analysis without language detection or manual review.
- Overwriting engagement counts without a timestamp.
Wrap-up
For reply collection, use POST /twitter/tweets/{tweet_id}/replies/page, keep sort_by stable, continue with next_cursor, and save the root tweet plus raw reply rows. The result is easier to audit, retry, and explain than a single large fixed-count request.