> ## 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.

# Export Twitter replies with a scraper API guide

> Export Twitter replies through a scraper API. Estimate credits, paginate reply rows, and save CSV, JSON, or XLSX files with exact Xquik API steps and errors.

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

Export Twitter replies as CSV, JSON, or XLSX with `reply_extractor`.
With `auto` or `complete`, collection follows parent links beneath non-root replies too.
Reply results exclude ancestors and unrelated conversation branches, including thread collection.
Set `includeOriginalPost` to include the target itself. Use `resultsLimit` to cap results.
Use the live replies API for current pages with authors and engagement counts.
The Reply Actor's automatic mode also recovers nested replies after empty searches.
Incomplete recovery keeps partial rows and reports `pagination_safety_limit`.

## When to use this workflow

<CardGroup cols={2}>
  <Card title="Spreadsheet export" icon="file-spreadsheet">
    Use `reply_extractor` plus CSV or XLSX export when analysts need reply rows.
  </Card>

  <Card title="App ingestion" icon="database">
    Use `reply_extractor` plus paginated JSON results for queues, CRMs, or warehouses.
  </Card>

  <Card title="Moderation queue" icon="list-check">
    Use the direct replies API plus JSON Lines rows before storing, hiding, labeling, or routing replies.
  </Card>

  <Card title="Latest page" icon="search">
    Use `GET /x/tweets/{id}/replies` when you only need the newest reply page.
  </Card>

  <Card title="Cost control" icon="gauge">
    Estimate first, then cap results with `resultsLimit`.
  </Card>
</CardGroup>

## Data you get

<CardGroup cols={2}>
  <Card title="Reply author" icon="user-round">
    User ID, username, display name, follower count, verified state, and profile image.
  </Card>

  <Card title="Reply tweet" icon="message-square-text">
    Tweet ID, tweet text, and tweet created time.
  </Card>

  <Card title="Engagement" icon="chart-no-axes-combined">
    Likes, reposts, replies, quotes, views, and bookmarks.
  </Card>

  <Card title="Metadata" icon="braces">
    Language, source app, and conversation ID.
  </Card>
</CardGroup>

For extraction jobs, `GET /extractions/{id}` returns `job`, `results`, `hasMore`, and `nextCursor`. Each reply row includes `xUserId`, `xUsername`, `xDisplayName`, `tweetId`, `tweetText`, `tweetCreatedAt`, `createdAt`, and optional `enrichmentData` fields such as `likeCount`, `replyCount`, `repostCount`, `quoteCount`, `viewCount`, `bookmarkCount`, `conversationId`, `lang`, and `source`.

## End-to-end reply export handoff

Store one checkpoint that carries the target tweet through estimate, job creation, JSON pagination, and file export:

```json theme={null}
{
  "workflow": "reply_export",
  "request": {
    "toolType": "reply_extractor",
    "targetTweetId": "1893704267862470862",
    "resultsLimit": 500
  },
  "estimate": {
    "estimatedResults": 1200,
    "creditsRequired": "1200",
    "creditsAvailable": "77000",
    "allowed": true,
    "source": "replyCount"
  },
  "create_receipt": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "toolType": "reply_extractor",
    "status": "running",
    "poll_path": "/api/v1/extractions/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  },
  "json_pages": {
    "limit": 1000,
    "page_cursor": null,
    "next_cursor": "990200",
    "has_more": true
  },
  "export_paths": {
    "csv": "/api/v1/extractions/a1b2c3d4-e5f6-7890-abcd-ef1234567890/export?format=csv",
    "json": "/api/v1/extractions/a1b2c3d4-e5f6-7890-abcd-ef1234567890/export?format=json",
    "xlsx": "/api/v1/extractions/a1b2c3d4-e5f6-7890-abcd-ef1234567890/export?format=xlsx"
  },
  "normalized_row": {
    "parent_tweet_id": "1893704267862470862",
    "reply_tweet_id": "1893710452812718080",
    "reply_text": "Thanks for the update.",
    "reply_created_at": "2026-05-01T10:05:00.000Z",
    "reply_author_id": "44196397",
    "reply_author_username": "username",
    "conversation_id": "1893704267862470862",
    "export_format": "csv"
  },
  "handoff_state": "poll_until_completed_then_export"
}
```

<CardGroup cols={2}>
  <Card title="Estimate checkpoint" icon="calculator">
    Keep the estimate response with `targetTweetId`.
    A successful count lookup sets `source` to `replyCount`.
  </Card>

  <Card title="Job checkpoint" icon="clipboard-check">
    Store the returned job `id`, `status`, and `poll_path`. Do not expect reply rows in the create response.
  </Card>

  <Card title="Cursor checkpoint" icon="shuffle">
    Store `page_cursor`, `next_cursor`, and `has_more` for JSON page loops, then pass `nextCursor` back as `cursor`.
  </Card>

  <Card title="Export checkpoint" icon="download">
    Store the chosen CSV, JSON, or XLSX `export_paths` and the normalized reply fields sent downstream.
  </Card>
</CardGroup>

## Step 1: estimate replies and credits

Call `POST /extractions/estimate` before scraping. `reply_extractor` requires
`targetTweetId`. The estimate uses the tweet's current `replyCount`, even when
you plan to cap the created job.

```bash theme={null}
curl -X POST https://xquik.com/api/v1/extractions/estimate \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "toolType": "reply_extractor",
    "targetTweetId": "1893704267862470862"
  }' | jq
```

The estimate returns `allowed`, `estimatedResults`, `creditsRequired`,
`creditsAvailable`, and `source`. A successful tweet count lookup sets `source`
to `replyCount`. Set `resultsLimit` on the create request
when you want a smaller sample or hard run cap. Final rows and credits can be
lower than the estimate.

## Step 2: run the reply extraction

Create the job with the same `toolType`, `targetTweetId`, and optional `resultsLimit`.

```bash theme={null}
curl -X POST https://xquik.com/api/v1/extractions \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "toolType": "reply_extractor",
    "targetTweetId": "1893704267862470862",
    "resultsLimit": 500
  }' | jq
```

```json theme={null}
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "toolType": "reply_extractor",
  "status": "running"
}
```

Store the creation response as a local handoff before polling:

```json theme={null}
{
  "job": "reply_extraction",
  "reply_extraction_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "target_tweet_id": "1893704267862470862",
  "results_limit": 500,
  "status": "running",
  "handoff_created_at": "2026-05-16T03:07:00.000Z"
}
```

Poll by `reply_extraction_id`. Keep `target_tweet_id` and `results_limit` with the audit record. Do not wait for `totalResults` or `createdAt` in the create response. Those fields arrive from `GET /extractions/{id}`.

## Step 3: poll job status

Poll `GET /extractions/{id}` until the job is `completed` or `failed`.

```bash theme={null}
curl https://xquik.com/api/v1/extractions/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
  -H "x-api-key: xq_YOUR_KEY_HERE" | jq
```

Use the paginated response when your app wants JSON rows instead of a file download. Use `limit` up to 1,000 and pass `nextCursor` as `cursor` until `hasMore` is `false`.

## Step 4: export CSV, JSON, or XLSX

File exports do not charge credits after job creation. Save `xquik-replies.jsonl` for queue replay or warehouse loads, `xquik-replies.json` for app ingestion, `xquik-replies.csv` for CRM import, and `xquik-replies.xlsx` for analyst handoff.

```bash theme={null}
curl -X GET "https://xquik.com/api/v1/extractions/a1b2c3d4-e5f6-7890-abcd-ef1234567890/export?format=csv" \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -o xquik-replies.csv
```

```bash theme={null}
curl -X GET "https://xquik.com/api/v1/extractions/a1b2c3d4-e5f6-7890-abcd-ef1234567890/export?format=xlsx" \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -o xquik-replies.xlsx
```

```bash theme={null}
curl -X GET "https://xquik.com/api/v1/extractions/a1b2c3d4-e5f6-7890-abcd-ef1234567890/export?format=json" \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -o xquik-replies.json
```

### Saved export JSON Lines handoff

Use this after `format=json` when a warehouse, queue, CRM, or AI agent needs one reply per line with stable field names.

```bash theme={null}
jq -c --arg extraction_id "a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
  '.[] | {
    job: "reply_export",
    extraction_id: $extraction_id,
    reply_tweet_id: .tweetId,
    reply_text: .tweetText,
    reply_author_id: .xUserId,
    reply_author_username: .xUsername,
    created_at: .tweetCreatedAt,
    conversation_id: .conversationId,
    like_count: .likeCount,
    reply_count: .replyCount,
    quote_count: .quoteCount,
    view_count: .viewCount,
    handoff_format: "jsonl"
  }' xquik-replies.json > xquik-replies.jsonl
```

## Direct replies API

Use `GET /x/tweets/{id}/replies` when you need a paginated API response instead of a stored extraction job.

```bash theme={null}
curl "https://xquik.com/api/v1/x/tweets/1893704267862470862/replies" \
  -H "x-api-key: xq_YOUR_KEY_HERE" | jq
```

The direct replies API returns `tweets`, `has_next_page`, and `next_cursor`. Pass `next_cursor` back as `cursor` to fetch the next page. It costs 1 credit per tweet returned.

Use `sinceTime` and `untilTime` as Unix timestamps in seconds when a moderation queue, CRM sync, or agent only needs replies from a specific window.

```bash theme={null}
curl -G https://xquik.com/api/v1/x/tweets/1893704267862470862/replies \
  --data-urlencode "sinceTime=1777392000" \
  --data-urlencode "untilTime=1777478400" \
  -H "x-api-key: xq_YOUR_KEY_HERE" | jq
```

## Copy-ready workflow: replies to moderation queue

Use this direct API workflow when you need the latest reply pages as JSON Lines before deciding what to store, hide, label, or route. Each row maps the API response into stable downstream fields: `handoff_source`, `parent_tweet_id`, `reply_tweet_id`, `reply_text`, `reply_author_id`, `reply_author_username`, `reply_author_name`, `reply_author_followers`, `reply_author_verified`, `reply_author_profile_picture`, `created_at`, `in_reply_to_id`, `conversation_id`, engagement counts, `bookmark_count`, `is_note_tweet`, `tweet_source`, `media_urls`, and cursor fields.

```javascript theme={null}
const apiKey = process.env.XQUIK_API_KEY;
const tweetId = "1893704267862470862";
let cursor;
let pageIndex = 0;

do {
  const url = new URL(`https://xquik.com/api/v1/x/tweets/${tweetId}/replies`);
  if (cursor) {
    url.searchParams.set("cursor", cursor);
  }

  const response = await fetch(url, {
    headers: { "x-api-key": apiKey },
  });

  if (response.status === 429) {
    throw new Error("Rate limited. Retry after the response header delay.");
  }
  if (!response.ok) {
    const body = await response.json();
    throw new Error(`${response.status} ${body.error}`);
  }

  const page = await response.json();
  for (const tweet of page.tweets ?? []) {
    const row = {
      handoff_source: "xquik.replies.direct",
      parent_tweet_id: tweetId,
      reply_tweet_id: tweet.id,
      reply_text: tweet.text,
      reply_author_id: tweet.author?.id ?? null,
      reply_author_username: tweet.author?.username ?? null,
      reply_author_name: tweet.author?.name ?? null,
      reply_author_followers: tweet.author?.followers ?? null,
      reply_author_verified: tweet.author?.verified ?? null,
      reply_author_profile_picture: tweet.author?.profilePicture ?? null,
      created_at: tweet.createdAt ?? null,
      in_reply_to_id: tweet.inReplyToId ?? null,
      conversation_id: tweet.conversationId ?? null,
      like_count: tweet.likeCount ?? 0,
      reply_count: tweet.replyCount ?? 0,
      retweet_count: tweet.retweetCount ?? 0,
      quote_count: tweet.quoteCount ?? 0,
      view_count: tweet.viewCount ?? 0,
      bookmark_count: tweet.bookmarkCount ?? 0,
      is_note_tweet: tweet.isNoteTweet ?? false,
      tweet_source: tweet.source ?? null,
      media_urls: (tweet.media ?? []).map((item) => item.mediaUrl).filter(Boolean),
      page_index: pageIndex,
      page_cursor: cursor ?? "",
      next_cursor: page.next_cursor,
      has_next_page: page.has_next_page,
    };
    process.stdout.write(JSON.stringify(row) + "\n");
  }

  cursor = page.has_next_page ? page.next_cursor : undefined;
  pageIndex += 1;
} while (cursor);
```

Use extraction jobs for saved files, credit estimates, or fixed `resultsLimit` caps.

## Choose the right reply output

### How do replies differ from quote tweets?

Replies stay within conversation threads beneath the original tweet. Quote
tweets create separate posts with added commentary. Store the original tweet
ID with every reply row. Use the
[Quote Tweets API](/api-reference/x/tweet-quotes) for quote tweets. Never merge
both result sets without a field that identifies their relationship type.

### Why can public replies be missing?

X can rank replies instead of showing them chronologically. It can also place
probable spam behind an extra control. Protected, deleted, and hidden replies
have separate visibility rules. Review the
[official X reply guidance](https://help.x.com/en/using-x/mentions-and-replies).
For API exports, follow every cursor and store each confirmed page. Treat a
`424 replies_incomplete` response as evidence against complete coverage.

### How should a support team moderate reply rows?

Keep reply text, author ID, username, creation time, and engagement counts.
Route uncertain rows to a human review queue. Add sentiment or spam labels with
your own classifier. Xquik does not return a sentiment verdict. CSV suits
spreadsheet review. JSON keeps nested media and author fields.

### When should I use the live replies API?

Use the Twitter reply API when an application needs the latest cursor page. It returns reply tweets, authors, engagement counts, media, and the next cursor. Continue until `has_next_page` is `false`. New replies can arrive during pagination. Store each reply tweet ID and remove duplicate IDs downstream.

### When should I run a saved reply extraction?

Choose `reply_extractor` for stored files and reviewable jobs. Estimate the parent tweet first. Then create one capped or uncapped extraction. Poll the returned job ID until completion. File conversion adds no credit charge.

### Which file format should I choose?

Choose CSV for CRM imports and spreadsheet filters. Choose XLSX for analyst handoffs that require workbook tools. Choose JSON for applications that keep nested reply fields. Convert JSON to JSON Lines when queues or warehouses need one reply per record.

### How do I resume an interrupted reply export?

Store the target tweet ID, extraction ID, current cursor, next cursor, and page index. Resume a saved job through `GET /extractions/{id}`. Resume live pagination with the last confirmed `next_cursor`. Never advance the checkpoint before storing its reply rows.

### How should I validate exported replies?

Check that every row has a reply tweet ID and parent tweet ID. Keep author IDs separately from usernames because usernames can change. Keep tweet creation times in UTC. Compare the returned rows with the estimate only for planning. The parent `replyCount` can change and does not guarantee the final export size.

## Cost and failure handling

Estimates are free. A `reply_extractor` job costs 1 credit per result. Direct `GET /x/tweets/{id}/replies` calls cost 1 credit per returned tweet. Available credits can reduce the returned page size. File exports do not charge credits after job creation.

Respect the read rate limit on live Twitter API pages. Use the parent reply count only for estimates. Trust the returned reply rows over the estimate.

Handle common errors before retrying:

<CardGroup cols={2}>
  <Card title="400 invalid tweet ID" icon="circle-alert">
    Status `400`. Error `invalid_tweet_id`. Send a post ID or post URL.
  </Card>

  <Card title="401 unauthenticated" icon="key-round">
    Status `401`. Error `unauthenticated`. Send a valid `x-api-key`.
  </Card>

  <Card title="402 billing or credits" icon="coins">
    Status `402`. Errors `no_subscription`, `subscription_inactive`, `no_credits`, or `insufficient_credits`. Subscribe, add credits, or lower `resultsLimit`.
  </Card>

  <Card title="429 rate limit" icon="timer-reset">
    Status `429`. Error `rate_limit_exceeded`. Wait for `retryAfter` or the `Retry-After` header.
  </Card>

  <Card title="424 or 502 upstream unavailable" icon="rotate-ccw">
    Status `424` or `502`. Error `x_api_unavailable`. Retry with exponential backoff.
  </Card>
</CardGroup>

## Handoff checklist

<CardGroup cols={2}>
  <Card title="Spreadsheet" icon="file-spreadsheet">
    Export `format=csv` to `xquik-replies.csv` or `format=xlsx` to `xquik-replies.xlsx`.
  </Card>

  <Card title="App ingestion" icon="database">
    Export `format=json` to `xquik-replies.json`, convert it to `xquik-replies.jsonl`, or paginate `GET /extractions/{id}`.
  </Card>

  <Card title="Cost control" icon="gauge">
    Set `resultsLimit` on create calls when you need a smaller run.
  </Card>

  <Card title="New reply alerts" icon="radio">
    Create an account or keyword monitor with `tweet.reply` events.
  </Card>
</CardGroup>

## Related reply APIs

* [Plan saved tweet and reply exports](/guides/extraction-workflow)
* [Create a reply extraction job](/api-reference/extractions/create)
* [Fetch live tweet reply pages](/api-reference/x/tweet-replies)


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