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

# X (Twitter) API TypeScript tweet search & CSV export

> Search tweets with the Xquik TypeScript SDK, follow cursors, and save rows as JSON Lines, CSV, or XLSX for workers, queues, and analysts in your own apps.

> For the complete documentation index, see [llms.txt](https://docs.xquik.com/llms.txt).

This guide builds on the [TypeScript SDK setup](https://docs.xquik.com/sdks/typescript.md), which covers install and authentication.

## Tweet search workflow to JSON Lines, CSV, or XLSX

This job is for Node.js, Bun, and queue workers that need searchable tweet data. They need it in a stable handoff format. The format is for queues, data lakes, analyst CSV files, or XLSX workbooks. It calls `GET /x/tweets/search` through `client.x.tweets.search` with the generated `TweetSearchParams` shape. It writes each returned tweet as one projected JSON object per line.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/bash.json","/languages/csharp.json","/languages/go.json","/languages/java.json","/languages/javascript.json","/languages/kotlin.json","/languages/php.json","/languages/python.json","/languages/ruby.json","/languages/typescript.json"]}}
import XTwitterScraper from "x-twitter-scraper";

const client = new XTwitterScraper();
const query = "from:username webhook OR SDK";
let cursor: string | undefined;
let pageIndex = 0;

while (true) {
  const pageCursor = cursor ?? null;
  const page = await client.x.tweets.search({
    q: query,
    queryType: "Latest",
    cursor,
  });

  for (const tweet of page.tweets) {
    const row = {
      source: "xquik.typescript.search",
      query,
      tweet_id: tweet.id,
      text: tweet.text,
      author_id: tweet.author?.id ?? null,
      author_username: tweet.author?.username ?? null,
      author_name: tweet.author?.name ?? null,
      created_at: tweet.createdAt ?? 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,
      page_index: pageIndex,
      page_cursor: pageCursor,
      next_cursor: page.next_cursor || null,
      has_next_page: page.has_next_page,
    };

    process.stdout.write(`${JSON.stringify(row)}\n`);
  }

  if (!page.has_next_page || !page.next_cursor) {
    break;
  }

  cursor = page.next_cursor;
  pageIndex += 1;
}
```

The generated params map directly to the REST endpoint.

> [card] q
>
> Maps to REST `q`. It holds the required X (Twitter) search query with keywords, handles, hashtags, or operators.

> [card] limit
>
> Maps to REST `limit`. Use it as a 1 to 200 upper bound for a bounded pull. If `page.has_next_page` is true, keep the same `q`, filters, `queryType`, and `limit`. That applies when you continue with `page.next_cursor`.

> [card] cursor
>
> Maps to REST `cursor`. Pass the opaque cursor from `page.next_cursor` to request the next page.

> [card] sinceTime
>
> Maps to REST `sinceTime`. Use the inclusive ISO bound on every page.

> [card] untilTime
>
> Maps to REST `untilTime`. Use the exclusive ISO bound on every page.

> [card] queryType
>
> Maps to REST `queryType`. Use `Latest` for chronological results or `Top` for engagement-ranked results.

## Returned data & handoff

`client.x.tweets.search` returns `PaginatedTweets`.

> [card] page.tweets
>
> JSON field `tweets`. Contains tweet records with `id`, `text`, optional `author`, and `createdAt`. Records also carry `likeCount`, `replyCount`, `retweetCount`, `quoteCount`, `bookmarkCount`, `viewCount`, and `isNoteTweet` when available.

> [card] page.has\_next\_page
>
> TypeScript field `page.has_next_page`. JSON field `has_next_page`. Tells your worker whether another page exists.

> [card] page.next\_cursor
>
> JSON field `next_cursor`. Store it only when `page.has_next_page` is true. For bounded pulls that return fewer tweets than `limit`, pass it back as `cursor`. Send the same query, filters, `queryType`, and `limit` with it.

Project `page.tweets` into JSON Lines rows in `xquik-tweet-search.jsonl` for queues and data lakes. You can also transform the same projected records into CSV for analysts. When account teams need spreadsheets, produce XLSX from those rows. Store `tweet_id`, `author_username`, engagement counts, `page_index`, `page_cursor`, `next_cursor`, and `has_next_page`. With them, workers resume from the last saved cursor without replaying raw SDK objects. For explicit `limit` pulls, resume with the same query, filters, `queryType`, and `limit`. Only `cursor` changes.

## Next steps

* [TypeScript SDK setup](https://docs.xquik.com/sdks/typescript.md)
* [Follower export](https://docs.xquik.com/sdks/typescript-follower-export.md)
* [Media tweets](https://docs.xquik.com/sdks/typescript-media-tweets.md)


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