Pagination
How TwittrAPI paginates with X's cursors, where to find the bottom cursor in the raw timeline JSON, and when to stop.
Timelines, followers, search and similar endpoints return one page per call. To get the next page, send the bottom cursor of the current page as the cursor query parameter. Each page is one billed call.
Endpoints that paginate say so on their reference page. They take count (default 20) and cursor.
Where the cursor is
The API passes X's JSON through, so the cursor sits where X puts it: in the timeline's instructions, as an entry whose entryId starts with cursor-bottom-:
{
"type": "TimelineAddEntries",
"entries": [
{ "entryId": "tweet-1790000000000000000", "content": { "...": "..." } },
{
"entryId": "cursor-bottom-1790000000000000000",
"content": {
"entryType": "TimelineTimelineCursor",
"cursorType": "Bottom",
"value": "DAABCgABGc..."
}
}
]
}Where the instructions array is depends on the endpoint. For example:
- User tweets:
data.user.result.timeline.timeline.instructions - Tweet replies:
data.threaded_conversation_with_injections_v2.instructions - Search: the API already unwraps it.
/searchreturns{"category": ..., "entries": [...]}andentriesis the instructions array.
On later pages X often moves the cursor into a TimelineReplaceEntry instruction ("entry": {...}) instead of the entries list. There is also a cursor-top- entry; ignore it unless you want to poll for newer items.
Because the paths differ and X changes them, the simplest robust approach is to search the whole response for an object with "cursorType": "Bottom" and take its value.
Find the cursor yourself
def bottom_cursor(node):
"""Returns the bottom cursor anywhere in X's JSON, or None."""
if isinstance(node, dict):
if node.get("cursorType") == "Bottom" and isinstance(node.get("value"), str):
return node["value"]
for value in node.values():
found = bottom_cursor(value)
if found:
return found
elif isinstance(node, list):
for item in node:
found = bottom_cursor(item)
if found:
return found
return Nonecurl 'https://api.twittrapi.com/user/12/followers?count=20&cursor=DAABCgABGc...' \
--header 'X-API-KEY: YOUR_API_KEY'curl 'https://twitter-aio.p.rapidapi.com/user/12/followers?count=20&cursor=DAABCgABGc...' \
--header 'x-rapidapi-host: twitter-aio.p.rapidapi.com' \
--header 'x-rapidapi-key: YOUR_API_KEY'Let the SDK do it
The SDKs have a paginator that finds the cursor and keeps calling:
from twittrapi import TwittrAPI
client = TwittrAPI()
for page in client.paginate(client.users.followers, "12", count=100, max_pages=5):
... # page is the raw JSON of one callfor await (const page of client.paginate(client.users.followers, { userId: "12", count: 100 }, { maxPages: 5 })) {
// page.data is the raw JSON of one call, page.nextCursor the cursor for the next
}extract_bottom_cursor (Python) and extractBottomCursor (Node.js) give you just the cursor if you page by hand.
When to stop
Stop when any of these is true:
- No bottom cursor in the response.
- The cursor is the same as the one you sent.
- The page has no entries other than cursors. X keeps returning a cursor at the end of a timeline, so this check matters.
- You have as many items or pages as you need. Always set a limit; every page costs a call.
Notes
countis passed to X as is. X decides the real page size and often returns a few more or fewer items.- Cursors are opaque strings from X. Store them as they are; they can stop working after a while.
/tweet/{tweetId}usescursorto load more replies of the conversation. Its page size is fixed by X.- Coming from
has_next_page,next_cursorormeta.next_token? The migration guides show the differences: from twitterapi.io, from SocialData, from the official X API.