> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xquik.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Twitter API pagination, batching & tweet exports

> Paginate Twitter API tweets, timelines, followers, and searches. Batch IDs, prevent duplicates, resume cursors, manage retries, and export CSV, JSON, or XLSX.

<blockquote className="agent-llms-directive">
  For the complete documentation index, see <a href="/llms.txt">llms.txt</a>.
</blockquote>

Use this Twitter API pagination guide for tweets, profiles, followers, and exports.
Choose one matching route. Save every returned ID and cursor.
Resume that checkpoint instead of repeating completed pages.

<Note>
  In short, batch known IDs. Use profile timelines for one account.
  Search tweets for keywords. Use extractions for saved CSV, JSON, or XLSX.
</Note>

## Choose the smallest Twitter API route

The smallest matching route reduces duplicate tweets and unnecessary requests.
Do not force every job through Twitter advanced search.

| Collection job | Route | Page control | Required account |
| - | - | - | - |
| Fetch known tweet IDs | `GET /x/tweets` | Up to 100 IDs | No connected X account |
| Fetch known user IDs | `GET /x/users/batch` | Up to 100 IDs | No connected X account |
| Get tweets by one user | `GET /x/users/{id}/tweets` | `pageSize` and `cursor` | No connected X account |
| Search tweets by keyword | `GET /x/tweets/search` | `limit` and `cursor` | No connected X account |
| Export followers or following | `GET /x/users/{id}/followers` or `/following` | `pageSize` and `cursor` | No connected X account |
| Read the home feed | `GET /x/timeline` | `cursor` and `seenTweetIds` | Connected X account |
| Save a durable file | `POST /extractions` | `resultsLimit`, then `cursor` | Depends on the selected tool |

Check each route before reusing a parameter name.
Twitter API endpoints do not share one page-size parameter.

<CardGroup cols={2}>
  <Card title="Known tweet IDs" icon="hash">
    Use `GET /api/v1/x/tweets?ids=...` for up to 100 comma-separated tweet IDs in one request.
  </Card>

  <Card title="Known user IDs" icon="users">
    Use `GET /api/v1/x/users/batch?ids=...` for up to 100 comma-separated user IDs in one request.
  </Card>

  <Card title="One profile timeline" icon="list-tree">
    Use `GET /api/v1/x/users/{id}/tweets` for one user's profile timeline. Pass a username or numeric X user ID.
  </Card>

  <Card title="Keyword or advanced search" icon="search">
    Use `GET /api/v1/x/tweets/search` for keywords, hashtags, operators, date filters, and advanced search pages.
  </Card>

  <Card title="Authenticated home timeline" icon="house">
    Use `GET /api/v1/x/timeline` for the connected account's home timeline. Pass `cursor` and optional `seenTweetIds`.
  </Card>

  <Card title="Saved file export" icon="download">
    Use `POST /api/v1/extractions/estimate`, `POST /api/v1/extractions`, and `GET /api/v1/extractions/{id}/export` for CSV, JSON, or XLSX.
  </Card>
</CardGroup>

## Batch known tweet or profile IDs

Batch known IDs before starting a cursor loop.
One batch accepts up to 100 comma-separated IDs.
Duplicate user IDs keep only their first position.

<CodeGroup>
  ```bash Tweets theme={null}
  curl "https://xquik.com/api/v1/x/tweets?ids=1893710452812718080,1893704267862470862" \
    -H "x-api-key: xq_YOUR_KEY_HERE"
  ```

  ```bash Users theme={null}
  curl "https://xquik.com/api/v1/x/users/batch?ids=44196397,783214" \
    -H "x-api-key: xq_YOUR_KEY_HERE"
  ```
</CodeGroup>

Keep the submitted ID list beside each response.
Match returned tweets or profiles by their stable string IDs.
Retry only tweet IDs missing from the completed response.

Batch profile responses expose reconciliation fields.
Inspect `requested_count`, `processed_count`, and `returned_count`.
Retry `failed_ids` now & `unprocessed_ids` after adding credits.
Skip `unavailable_ids`, which have no profile on X.

Batch lookups are single-page requests.
Do not send an empty `next_cursor` into another batch request.

## Match Twitter search, timeline & feed intent

These routes can return similar tweet objects.
Their collection intent remains different.

<CardGroup cols={3}>
  <Card title="Profile timeline" icon="user-round">
    Use `/x/users/{id}/tweets` for one account's posts. Add `includeReplies`
    when replies belong in the result.
  </Card>

  <Card title="Tweet search" icon="search">
    Use `/x/tweets/search` for keywords, hashtags, operators, dates, or
    engagement filters.
  </Card>

  <Card title="Home timeline" icon="house">
    Use `/x/timeline` for one connected account's ranked home feed.
    It is not a keyword search route.
  </Card>
</CardGroup>

Use profile timelines for complete account-oriented collection.
Use tweet search when the query meaning matters most.
Use home timelines for feed-style inboxes or routing.

Plain `from:username` date searches can use timeline-oriented collection.
Add keywords when ranked search semantics matter more.
Keep `q`, filters, dates, and `queryType` unchanged across pages.

## Use the correct page-size parameter

Page-size names vary across Xquik routes.
Copy the parameter from that endpoint's API reference.

| Route family | Request parameter | Range or behavior |
| - | - | - |
| Tweet search | `limit` | 1 to 10,000 tweets. Default 20 |
| User tweets and replies | `pageSize` | Automatic 1 to 300. Standard 1 to 100. Default 20 |
| Followers and following | `pageSize` | Automatic 20 to 300. Standard 20 to 200. Default 200 |
| Home timeline | None | The source controls each page size |
| Extraction results | `limit` | 1 to 1,000 stored rows. Default 100 |

A requested size is an upper bound.
Filters, source availability, or credits can reduce returned rows.
Continue while the response says another page exists.

Larger pages reduce HTTP calls.
Smaller pages reduce memory and checkpoint loss after failures.
Choose the largest size your worker can safely store atomically.

## Use extraction jobs for saved files

Use extractions for durable Twitter scraper API jobs.
They support saved results and repeatable file handoffs.

<Steps>
  <Step title="Estimate">
    Send the planned tool, target, and `resultsLimit`.
    Review the estimate before creating the job.
  </Step>

  <Step title="Create">
    Call `POST /api/v1/extractions`.
    Store its job `id`, `status`, and poll path.
  </Step>

  <Step title="Poll JSON">
    Poll `GET /api/v1/extractions/{id}` until completion.
    Pass `nextCursor` back through `cursor` for more stored rows.
  </Step>

  <Step title="Export">
    Download CSV, JSON, Markdown, Markdown document, PDF, TXT, or XLSX.
    Check the export page's row and format limits first.
  </Step>
</Steps>

Direct routes suit live application pages.
Extractions suit durable exports, analysts, and asynchronous workflows.

| Need | Prefer direct API | Prefer extraction |
| - | - | - |
| Show one live page | Yes | No |
| Resume a short cursor loop | Yes | Optional |
| Download CSV or XLSX | No | Yes |
| Save a durable job record | No | Yes |
| Stream more rows than a file cap | Yes, or stored result pages | Use stored result pages |

Store the export format, job ID, row count, and completion state.
Do not call the export route before the job completes.

## Store cursor checkpoints

Store the request and response cursor together.
Treat each cursor as an opaque string.
Never decode, trim, or construct one.

```json theme={null}
{
  "route": "/api/v1/x/tweets/search",
  "query": "from:username webhook OR SDK",
  "query_type": "Latest",
  "limit": 100,
  "cursor_sent": null,
  "has_next_page": true,
  "next_cursor": "DAACCgACGRElMJcAAA",
  "unique_tweet_count": 93,
  "last_saved_tweet_id": "1893710452812718080",
  "collected_at": "2026-08-02T20:30:00Z"
}
```

Most X reads return `next_cursor` and accept `cursor`.
Stored extraction pages return `nextCursor` and accept `cursor`.
Events and draws also accept `cursor`. Radar accepts `after`.
Drafts accept `afterCursor`.

The normalized REST contract uses `has_more` and `next_cursor`.
Each route still keeps its documented request parameter.

Write the rows and checkpoint in one database transaction.
Advance only after the row write succeeds.
Keep the previous checkpoint until validating its replacement.

## Implement a bounded tweet search loop

This TypeScript example keeps query intent across cursor pages.
It also stops repeated cursors and duplicate tweet rows.

```typescript theme={null}
type Tweet = {
  id: string;
  text?: string;
};

type TweetPage = {
  tweets: Tweet[];
  has_next_page: boolean;
  next_cursor?: string;
};

type SearchCheckpoint = {
  cursor_sent: string | null;
  next_cursor: string | null;
  page_number: number;
  unique_tweet_count: number;
};

type SearchCollectionResult = {
  complete: boolean;
  next_cursor: string | null;
  unique_tweet_count: number;
};

async function collectTweetSearch(
  apiKey: string,
  query: string,
  savePage: (tweets: Tweet[], checkpoint: SearchCheckpoint) => Promise<void>,
): Promise<SearchCollectionResult> {
  const seenTweetIds = new Set<string>();
  const seenCursors = new Set<string>();
  let cursor = "";

  for (let pageNumber = 1; pageNumber <= 50; pageNumber += 1) {
    const params = new URLSearchParams({
      limit: "100",
      q: query,
      queryType: "Latest",
    });
    if (cursor !== "") {
      params.set("cursor", cursor);
    }

    const response = await fetch(`https://xquik.com/api/v1/x/tweets/search?${params}`, {
      headers: { "x-api-key": apiKey },
    });
    if (!response.ok) {
      throw new Error(`Tweet search failed with ${response.status}`);
    }

    const page = (await response.json()) as TweetPage;
    const uniqueTweets = page.tweets.filter((tweet) => {
      if (seenTweetIds.has(tweet.id)) {
        return false;
      }
      seenTweetIds.add(tweet.id);
      return true;
    });

    const nextCursor = page.next_cursor || null;
    if (
      page.has_next_page &&
      nextCursor !== null &&
      (nextCursor === cursor || seenCursors.has(nextCursor))
    ) {
      throw new Error("Pagination stopped because the cursor repeated");
    }

    await savePage(uniqueTweets, {
      cursor_sent: cursor || null,
      next_cursor: nextCursor,
      page_number: pageNumber,
      unique_tweet_count: seenTweetIds.size,
    });

    if (!page.has_next_page) {
      return {
        complete: true,
        next_cursor: null,
        unique_tweet_count: seenTweetIds.size,
      };
    }

    if (nextCursor === null) {
      return {
        complete: false,
        next_cursor: null,
        unique_tweet_count: seenTweetIds.size,
      };
    }

    seenCursors.add(nextCursor);
    cursor = nextCursor;
  }

  return {
    complete: false,
    next_cursor: cursor,
    unique_tweet_count: seenTweetIds.size,
  };
}
```

Move status-specific recovery outside this collection function.
Resume only after correcting the failed condition.

## Guard high-volume Twitter API pagination

Bound every high-volume tweet scraper loop.
A filter can produce an empty page before later matches.
An empty page does not prove pagination finished.

<Steps>
  <Step title="Bound the run">
    Set maximum rows, pages, elapsed time, and expected credits.
  </Step>

  <Step title="Deduplicate rows">
    Store tweets by tweet ID. Store profiles by user ID.
  </Step>

  <Step title="Follow advancing cursors">
    Continue while the response reports more pages.
    Permit empty filtered pages when the cursor advances.
  </Step>

  <Step title="Stop stalled pagination">
    Stop when the next cursor is missing, unchanged, or previously seen.
  </Step>

  <Step title="Store after each page">
    Save both cursors, unique rows, and the last stable ID.
  </Step>
</Steps>

Schedule multiple accounts or searches fairly.
Fetch one bounded page slice from each stream.
Then resume deeper cursors in later rounds.
One large timeline should not block every other target.

For agent calls, return counts or bounded field projections.
API MCP output is limited to 24,000 characters.
Use REST, SDKs, or exports for every complete row.

## Resume recurring tweet collection

Do not restart recurring searches from their first page.
Save the last accepted tweet timestamp and stable ID.

For time-based searches, pass `sinceTime` and `untilTime`.
Use a small overlap between runs.
Then deduplicate overlapping tweets by tweet ID.

For recurring account or keyword checks, consider monitors.
Signed webhooks push matching tweet or profile events.
The events API supports replay and reconciliation.

Keep separate checkpoints for each route and query.
Changing filters creates a different result stream.
Never reuse a cursor after changing its query.

The [official X pagination guide](https://docs.x.com/x-api/fundamentals/pagination)
confirms 2 rules.
Pagination tokens are opaque. Short pages can still have successors.
Xquik applies both rules through its documented cursor fields.

## Recover without losing the cursor

Use the HTTP status before deciding whether to retry.

| Status | Meaning for this workflow | Correct next action |
| - | - | - |
| `400` | A parameter or query is invalid | Fix the request. Do not retry unchanged. |
| `401` | Authentication failed | Replace or correct the credential. |
| `402` | Credits cannot fund the requested work | Add credits or reduce future work. Resume the saved cursor. |
| `404` | The requested user, tweet, or job is unavailable | Record the missing target. Stop that stream. |
| `424` | An upstream dependency failed | Retry the same saved cursor with bounded backoff. |
| `429` | A request bucket or cooldown was exceeded | Wait for `Retry-After`. Retry the same cursor. |
| `502` | The X read dependency failed | Retry the same cursor with capped exponential backoff. |

Never advance the checkpoint after a failed response.
Never charge a failed page to the unique-row count.
Record partial completion when a retry budget expires.

## Control credits, requests & memory

Estimate work before starting large exports.
Multiply expected unique rows by the route's documented result cost.
Keep rate-limit budgets separate from credit budgets.

Requested rows can exceed affordable rows.
A paid endpoint can return a smaller page.
Zero affordable paid results return `402 insufficient_credits`.

Use these controls before every large run:

* Maximum unique tweets or profiles.
* Maximum cursor pages.
* Maximum elapsed time.
* Maximum expected credits.
* Maximum retry attempts per cursor.
* Maximum in-memory rows before flushing.

See [Twitter API rate limits](/guides/rate-limits) for request pacing.
See [pricing and billing](/guides/billing) for metered result rules.

## Avoid unnecessary media work

Pass public media URLs directly when creating tweets.
One public MP4 URL can also use the `media` field.
Do not upload already public tweet media first.

Upload media when a direct message needs a `mediaId`.
DM writes accept one item in `media_ids`.

Keep tweet media URLs separate from DM media IDs.

## Twitter API pagination questions

### How do I paginate Twitter API tweets?

Read `has_next_page` and save `next_cursor`.
Send that value as the next request's `cursor`.
Stop only when the response reports no next page.

### Can I get every tweet in one API request?

No. Collection routes return bounded pages.
Tweet search can request up to 10,000 results through `limit`.
Larger collections still require cursors or extraction jobs.

### Why did the API return fewer tweets than requested?

Page size is an upper bound.
Filters, source availability, and remaining credits can reduce results.
Continue whenever `has_next_page` remains true.

### How do I get tweets by one user efficiently?

Use `GET /x/users/{id}/tweets` with `pageSize` and `cursor`.
Add `includeReplies=true` only when replies belong in scope.
Use search when keywords or advanced filters define the job.

### Should I use a cursor or a timestamp?

Use cursors within one continuous collection run.
Use timestamps to define windows between recurring runs.
Apply a small overlap and deduplicate by tweet ID.

### How do I prevent duplicate tweets across pages?

Store every tweet ID in a unique index.
Track cursors separately from tweet IDs.
Reject repeated cursors before requesting another page.

### Should I use direct API pages or a Twitter export?

Use direct pages for live application responses.
Use extractions for durable CSV, JSON, or XLSX handoffs.
Use stored extraction pages when streaming beyond file limits.

## Efficient Twitter API usage checklist

* Choose the route matching IDs, users, search, feed, or export intent.
* Batch up to 100 known tweet or user IDs.
* Keep exact filters throughout every cursor run.
* Store rows and checkpoints atomically.
* Count unique IDs instead of raw array lengths.
* Continue through short or empty pages when cursors advance.
* Stop missing, unchanged, or repeated cursors.
* Bound pages, rows, time, credits, retries, and memory.
* Use `resultsLimit` for extraction estimates and jobs.
* Resume the same cursor after recoverable errors.
* Use monitors and webhooks for recurring checks.
* Keep tweet media URLs separate from uploaded DM media IDs.

<CardGroup cols={3}>
  <Card title="Tweet search API" icon="search" href="/api-reference/x/search-tweets">
    Search tweets by keywords, dates, authors, media, or engagement.
  </Card>

  <Card title="Extraction workflow" icon="download" href="/guides/extraction-workflow">
    Estimate, create, poll, paginate, and export durable jobs.
  </Card>

  <Card title="Rate limits" icon="gauge" href="/guides/rate-limits">
    Pace requests and recover from `429` responses safely.
  </Card>
</CardGroup>

<div className="related-api-links">
  <Accordion title="Related tweet, reply & media APIs" icon="link">
    * Tweets: [Get tweet](/api-reference/x/get-tweet) · [Batch tweets](/api-reference/x/batch-tweets) · [Tweet thread](/api-reference/x/tweet-thread) · [Hidden replies](/api-reference/x/tweet-hidden-replies) · [Translate tweet](/api-reference/x/tweet-translation) · [Embed tweet](/api-reference/x/tweet-embed) · [Resolve links](/api-reference/x/resolve-links) · [Tweet subtitles](/api-reference/x/tweet-subtitles) · [X Article](/api-reference/x/get-article)
    * Analysis: [Sentiment analysis](/api-reference/x/sentiment-analysis) · [Brand mentions](/api-reference/x/brand-monitoring) · [News classification](/api-reference/x/news-classification) · [Market signals](/api-reference/x/market-signals) · [Viral score](/api-reference/x/viral-score) · [Classify posts](/api-reference/x/classify-tweets)
    * Engagement: [Tweet replies](/api-reference/x/tweet-replies) · [Quote tweets](/api-reference/x/tweet-quotes) · [Likers](/api-reference/x/favoriters) · [Reposters](/api-reference/x/retweeters) · [Check repost](/api-reference/x/tweet-repost-check)
    * Profiles: [User tweets](/api-reference/x/user-tweets) · [Batch user tweets](/api-reference/x/batch-user-tweets) · [User replies](/api-reference/x/user-replies) · [User likes](/api-reference/x/user-likes) · [User media](/api-reference/x/user-media) · [User highlights](/api-reference/x/user-highlights) · [User articles](/api-reference/x/user-articles)
    * Feeds: [List tweets](/api-reference/x/list-tweets) · [Trends](/api-reference/x/trends) · [Trend locations](/api-reference/x/trend-locations) · [Search Spaces](/api-reference/x/search-spaces) · [Get Space](/api-reference/x/get-space) · [Space replay](/api-reference/x/space-replay) · [Get broadcast](/api-reference/x/get-broadcast) · [Hashflags](/api-reference/x/hashflags) · [Search places](/api-reference/x/search-places) · [Download media](/api-reference/x/download-media)
  </Accordion>
</div>

<div className="related-api-links">
  <Accordion title="Related timeline, bookmark & notification APIs" icon="link">
    * Account feeds: [Home timeline](/api-reference/x/timeline) · [Notifications](/api-reference/x/notifications) · [Mentions](/api-reference/x/user-mentions)
    * Saved and private: [Bookmarks](/api-reference/x/bookmarks) · [Bookmark folders](/api-reference/x/bookmark-folders) · [DM history](/api-reference/x/dm-history)
  </Accordion>
</div>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.