Docs
Pagination

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. /search returns {"category": ..., "entries": [...]} and entries is 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 None
curl 'https://api.twittrapi.com/user/12/followers?count=20&cursor=DAABCgABGc...' \
  --header 'X-API-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 call
for 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

  • count is 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} uses cursor to load more replies of the conversation. Its page size is fixed by X.
  • Coming from has_next_page, next_cursor or meta.next_token? The migration guides show the differences: from twitterapi.io, from SocialData, from the official X API.