> ## 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 Ruby tweet search to CSV & JSONL

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

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

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

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

Search with `client.x.tweets.search`, `GET /x/tweets/search`, and generated `XTwitterScraper::X::TweetSearchParams`. Save CSV and JSON Lines for Ruby workers, Rails, Sidekiq, agents, warehouses, and replay.

```ruby 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"]}}
require "csv"
require "json"
require "x_twitter_scraper"

client = XTwitterScraper::Client.new(
  api_key: ENV["X_TWITTER_SCRAPER_API_KEY"]
)

query = "from:username webhook OR SDK"
cursor = nil
page_index = 0

headers = [
  "source",
  "query",
  "tweet_id",
  "text",
  "author_id",
  "author_username",
  "author_name",
  "created_at",
  "like_count",
  "reply_count",
  "retweet_count",
  "quote_count",
  "view_count",
  "bookmark_count",
  "is_note_tweet",
  "page_index",
  "page_cursor",
  "next_cursor",
  "has_next_page"
]

CSV.open("xquik-tweet-search.csv", "w", write_headers: true, headers: headers) do |csv|
  File.open("xquik-tweet-search.jsonl", "w") do |jsonl|
    loop do
      page_cursor = cursor
      page = client.x.tweets.search(
        q: query,
        query_type: :Latest,
        cursor: cursor
      )

      page.tweets.each do |tweet|
        row = {
          "source" => "xquik.ruby.search",
          "query" => query,
          "tweet_id" => tweet.id,
          "text" => tweet.text,
          "author_id" => tweet.author&.id,
          "author_username" => tweet.author&.username,
          "author_name" => tweet.author&.name,
          "created_at" => tweet.created_at,
          "like_count" => tweet.like_count || 0,
          "reply_count" => tweet.reply_count || 0,
          "retweet_count" => tweet.retweet_count || 0,
          "quote_count" => tweet.quote_count || 0,
          "view_count" => tweet.view_count || 0,
          "bookmark_count" => tweet.bookmark_count || 0,
          "is_note_tweet" => tweet.is_note_tweet || false,
          "page_index" => page_index,
          "page_cursor" => page_cursor,
          "next_cursor" => page.next_cursor == "" ? nil : page.next_cursor,
          "has_next_page" => page.has_next_page
        }

        csv << headers.map { |header| row.fetch(header) }
        jsonl.puts(JSON.generate(row))
      end

      break unless page.has_next_page && page.next_cursor != ""

      cursor = page.next_cursor
      page_index += 1
    end
  end
end
```

The generated params map directly to the REST endpoint.

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

> [card] limit
>
> Ruby keyword `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, `query_type`, and `limit`. That applies when you continue with `page.next_cursor`.

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

> [card] since\_time
>
> Ruby keyword `since_time` maps to REST `sinceTime`. Use it as the ISO 8601 lower time bound.

> [card] until\_time
>
> Ruby keyword `until_time` maps to REST `untilTime`. Use it as the ISO 8601 upper time bound.

> [card] query\_type
>
> Ruby keyword `query_type` maps to REST `queryType`. Use `:Latest` for chronological results or `:Top` for engagement-ranked results.

## Returned data & handoff

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

> [card] page.tweets
>
> JSON field `tweets`. Contains `SearchTweet` records with `id`, `text`, optional `author`, and `created_at`. Records also carry `like_count`, `reply_count`, `retweet_count`, `quote_count`, `bookmark_count`, `view_count`, and `is_note_tweet` when available.

> [card] page.has\_next\_page
>
> Ruby 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, `query_type`, and `limit` with it.

Project `page.tweets` into CSV rows for analysts. Project them into JSON Lines rows for queues and data lakes. Store `tweet_id`, `author_username`, engagement counts, `page_index`, `page_cursor`, `next_cursor`, and `has_next_page` in `xquik-tweet-search.jsonl`. Workers can then resume. Or they can load the same records into XLSX, CRM, warehouse, or agent workflows. For explicit `limit` pulls, resume with the same query, filters, `query_type`, and `limit`. Only `cursor` changes.

## Next steps

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


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