Skip to main content

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.

Use the Ruby SDK for Ruby 3.2+ applications that need typed REST access, retries, Yard docs, RBS, RBI, and connection pooling. Use this page when you need a Ruby app, Rails job, or Sidekiq worker to search tweets, scrape tweets to CSV, JSON Lines, or XLSX, export followers, upload media, send direct messages, monitor tweets, or hand X data to a warehouse, CRM, queue, or agent workflow.

Install

gem install x-twitter-scraper
Or add it to your Gemfile:
gem "x-twitter-scraper", "~> 0.4.0"

Authenticate

export X_TWITTER_SCRAPER_API_KEY="xq_YOUR_KEY_HERE"

Basic Example

Search tweets and write durable JSON Lines handoff rows:
require "json"
require "x_twitter_scraper"

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

page = client.x.tweets.search(
  q: "from:xquikcom webhook OR SDK",
  limit: 10
)

page.tweets.each do |tweet|
  row = {
    tweet_id: tweet.id,
    text: tweet.text,
    author_username: tweet.author&.username,
    created_at: tweet.created_at
  }

  puts(JSON.generate(row))
end

Workflow: Search Tweets to CSV, JSON Lines, or XLSX

This job is for Ruby workers, Rails jobs, Sidekiq queues, and agent tools that need tweet search results in durable handoff files. It calls GET /x/tweets/search through client.x.tweets.search, uses the generated XTwitterScraper::X::TweetSearchParams shape, and writes analyst-friendly CSV plus JSON Lines for queues, warehouses, and replayable processing.
require "csv"
require "json"
require "x_twitter_scraper"

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

query = "from:xquikcom 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:

q

Ruby keyword q maps to REST q. Use it for the required X search query with keywords, handles, hashtags, or operators.

limit

Ruby keyword limit maps to REST limit. Use it for a bounded request from 1 to 200. Omit it for cursor loops.

cursor

Ruby keyword cursor maps to REST cursor. Pass the opaque cursor from page.next_cursor to request the next page.

since_time

Ruby keyword since_time maps to REST sinceTime. Use it as the ISO 8601 lower time bound.

until_time

Ruby keyword until_time maps to REST untilTime. Use it as the ISO 8601 upper time bound.

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:

page.tweets

JSON field tweets. Contains SearchTweet records with id, text, optional author, created_at, like_count, reply_count, retweet_count, quote_count, bookmark_count, view_count, and is_note_tweet when available.

page.has_next_page

Ruby field page.has_next_page. JSON field has_next_page. Tells your worker whether another page exists.

page.next_cursor

JSON field next_cursor. Store it with the job checkpoint and pass it back as cursor.
Project page.tweets into CSV rows for analysts and 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 so workers can resume safely or load the same records into XLSX, CRM, warehouse, or agent workflows.

Workflow: Follower Export to CSV, JSON, or XLSX

Use this workflow when a Ruby app, Rails job, Sidekiq worker, or agent tool needs an owned follower list for a CRM import, warehouse load, analyst CSV file, XLSX workbook, or resumable JSON handoff. It calls POST /extractions/estimate through client.extractions.estimate_cost, creates the job with client.extractions.run, reads saved rows with client.extractions.retrieve, and downloads files with client.extractions.export_results.
require "json"
require "x_twitter_scraper"

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

estimate = client.extractions.estimate_cost(
  tool_type: :follower_explorer,
  target_username: target_username
)

raise "Insufficient credits for follower export." unless estimate.allowed

job = client.extractions.run(
  tool_type: :follower_explorer,
  target_username: target_username
)

loop do
  status_page = client.extractions.retrieve(job.id, limit: 1)
  status = status_page.job[:status] || status_page.job["status"]

  break if status == "completed"

  raise "Follower export failed." if status == "failed"

  sleep 10
end

after = nil

File.open("xquik-followers.jsonl", "w") do |jsonl|
  loop do
    page = client.extractions.retrieve(job.id, limit: 1000, after: after)

    page.results.each do |row|
      jsonl.puts(JSON.generate(row))
    end

    break unless page.has_more && page.next_cursor.to_s != ""

    after = page.next_cursor
  end
end

csv_response = client.extractions.export_results(job.id, format_: :csv)
csv_response.rewind
File.binwrite("xquik-followers.csv", csv_response.read)

json_response = client.extractions.export_results(job.id, format_: :json)
json_response.rewind
File.binwrite("xquik-followers.json", json_response.read)

xlsx_response = client.extractions.export_results(job.id, format_: :xlsx)
xlsx_response.rewind
File.binwrite("xquik-followers.xlsx", xlsx_response.read)
follower_explorer requires target_username. Persist job.id, target_username, estimate.estimated_results, and estimate.source before polling so a Sidekiq retry, Rails job retry, or worker restart can resume the same follower export. client.extractions.retrieve returns results, has_more, and next_cursor; pass next_cursor back as after when you need stored JSON pages before exporting files. Map exported User ID or row xUserId as the CRM unique key. Use xquik-followers.jsonl for queue replay or warehouse loads, xquik-followers.json for app ingestion, xquik-followers.csv for CRM import, and xquik-followers.xlsx for analyst handoff. Cost: 1 credit per follower extracted or returned. Exports are free after the extraction job exists.

Workflow: Tweet Replies to CSV, JSON, or XLSX

Use this workflow when a Ruby app, Rails job, Sidekiq worker, or agent tool needs every reply under one tweet as a saved extraction, JSON Lines handoff, or CSV/JSON/XLSX file export. It calls POST /extractions/estimate through client.extractions.estimate_cost, creates the job with client.extractions.run, reads rows with client.extractions.retrieve, and downloads files with client.extractions.export_results.
require "json"
require "x_twitter_scraper"

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

estimate = client.extractions.estimate_cost(
  tool_type: :reply_extractor,
  target_tweet_id: target_tweet_id
)

raise "Insufficient credits for reply extraction." unless estimate.allowed

job = client.extractions.run(
  tool_type: :reply_extractor,
  target_tweet_id: target_tweet_id
)

loop do
  status_page = client.extractions.retrieve(job.id, limit: 1)
  status = status_page.job[:status] || status_page.job["status"]

  break if status == "completed"

  raise "Reply extraction failed." if status == "failed"

  sleep 3
end

after = nil

File.open("xquik-replies.jsonl", "w") do |jsonl|
  loop do
    page = client.extractions.retrieve(job.id, limit: 1000, after: after)

    page.results.each do |row|
      jsonl.puts(JSON.generate(row))
    end

    break unless page.has_more && page.next_cursor.to_s != ""

    after = page.next_cursor
  end
end

csv_response = client.extractions.export_results(job.id, format_: :csv)
csv_response.rewind
File.binwrite("xquik-replies.csv", csv_response.read)

json_response = client.extractions.export_results(job.id, format_: :json)
json_response.rewind
File.binwrite("xquik-replies.json", json_response.read)

xlsx_response = client.extractions.export_results(job.id, format_: :xlsx)
xlsx_response.rewind
File.binwrite("xquik-replies.xlsx", xlsx_response.read)
reply_extractor requires target_tweet_id. client.extractions.retrieve returns results, has_more, and next_cursor; store those values when you need resumable JSON pagination. Use 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. client.extractions.export_results supports :csv, :json, and :xlsx for file handoff. Cost: 1 credit per reply extracted or returned.

Workflow: Post Media Tweets and DM Attachments

Use this workflow when a Ruby app, Rails job, Sidekiq worker, or agent service needs to post a media-backed tweet, reply with media, or send one uploaded media item in a DM. Tweet and reply media posts use public media URLs directly on POST /x/tweets. Send up to 4 image URLs or exactly 1 MP4 video URL up to 100 MB. Do not mix video with other media. Do not upload first when the media URL is already public. Use client.request when a write worker must branch on the REST 202 x_write_unconfirmed response. The generated client.x.tweets.create helper is still useful when your flow only continues after a confirmed tweet.tweet_id.
def create_tweet_handoff(client, payload)
  response = client.request(
    method: :post,
    path: "x/tweets",
    body: payload
  )

  if response.fetch(:error, nil) == "x_write_unconfirmed"
    return {
      "status" => response.fetch(:status),
      "write_action_id" => response.fetch(:writeActionId),
      "charged" => response.fetch(:charged),
      "charged_credits" => response.fetch(:chargedCredits),
      "retryable" => response.fetch(:retryable),
      "poll" => "GET /x/write-actions/{id}"
    }
  end

  handoff = {
    "status" => "success",
    "tweet_id" => response.fetch(:tweetId),
    "charged" => response.fetch(:charged),
    "charged_credits" => response.fetch(:chargedCredits)
  }
  handoff["write_action_id"] = response.fetch(:writeActionId) if response.key?(:writeActionId)
  handoff
end

tweet_handoff = create_tweet_handoff(
  client,
  {
    account: "@xquikcom",
    text: "New demo video is live.",
    media: ["https://example.com/product-demo.mp4"]
  }
)

puts(JSON.generate(tweet_handoff))
To post an image reply, add reply_to_tweet_id:
reply_handoff = create_tweet_handoff(
  client,
  {
    account: "@xquikcom",
    text: "Here is the requested screenshot.",
    reply_to_tweet_id: "1893704267862470862",
    media: ["https://example.com/export-preview.png"]
  }
)

puts(JSON.generate(reply_handoff))
For DM attachments, upload the local file first and pass the returned media.media_id as the only media_ids item:
File.open("./handoff.png", "rb") do |file|
  media = client.x.media.upload(
    account: "@xquikcom",
    file: file
  )

  dm = client.x.dm.send_(
    "44196397",
    account: "@xquikcom",
    text: "Here is the asset.",
    media_ids: [media.media_id]
  )

  puts(dm.message_id)
end
Store write_action_id and charged_credits, then poll Get Write Action Status before retrying a pending tweet or reply. client.x.tweets.create returns tweet.tweet_id for confirmed-only flows. client.x.media.upload returns media.media_id for DM attachments, and client.x.dm.send_ returns dm.message_id for support tickets, CRM records, queue jobs, or agent memory. Text-only tweet and reply writes cost 10 credits. Tweet media adds 2 credits per started MB across attached files. Uploading media costs 10 credits, and sending the DM costs 10 credits. Do not pass uploaded media.media_id values to client.x.tweets.create; that method uses media with public media URLs.

Cost, Limits & Retries

Tweet search costs 1 credit per tweet returned. If remaining credits cannot cover a bounded limit request, the API can return fewer tweets; if 0 paid results are affordable, it returns 402 insufficient_credits. Read calls are rate-limited, and 429 responses include Retry-After. The client retries connection errors, timeouts, 408, 409, 429, and 5xx responses by default. Handle XTwitterScraper::Errors::RateLimitError with backoff, and fix 400, 401, 403, 404, or 422 responses before retrying.

Error Handling

All SDK errors inherit from XTwitterScraper::Errors::APIError.

400 Bad Request

Throws BadRequestError.

401 Unauthenticated

Throws AuthenticationError.

403 Permission Denied

Throws PermissionDeniedError.

404 Not Found

Throws NotFoundError.

422 Unprocessable Entity

Throws UnprocessableEntityError.

429 Rate Limited

Throws RateLimitError.

5xx Server Error

Throws InternalServerError.
begin
  account = client.account.retrieve
rescue XTwitterScraper::Errors::APIConnectionError => e
  warn("Connection failed: #{e.cause}")
rescue XTwitterScraper::Errors::APIStatusError => e
  warn("HTTP #{e.status}")
end

Pagination

Paginated responses expose fields such as has_next_page. Pass the endpoint’s cursor fields when requesting more pages.
page = client.x.tweets.search(q: "xquik", limit: 20)
puts("More results are available") if page.has_next_page

Webhooks & References

Last modified on May 20, 2026