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 CLI when a shell script, cron job, CI workflow, or operations runbook needs X API data without an application wrapper. It can search tweets, scrape tweets to JSON Lines, CSV, or XLSX, export follower data, upload media, post media tweets, post tweet replies, send direct messages, monitor tweets, and hand results to jq, Python, a warehouse loader, or a queue worker.

Install

go install github.com/Xquik-dev/x-twitter-scraper-cli/cmd/x-twitter-scraper@latest
Make sure your Go bin directory is on PATH:
export PATH="$PATH:$(go env GOPATH)/bin"

Authenticate

export X_TWITTER_SCRAPER_API_KEY="xq_YOUR_KEY_HERE"
You can also pass --api-key per command or use X_TWITTER_SCRAPER_BEARER_TOKEN for OAuth 2.1 access tokens.

Basic Example

x-twitter-scraper x:tweets search \
  --q from:elonmusk \
  --limit 10 \
  --format json
Use --help at any level:
x-twitter-scraper x:tweets search --help

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

Use this workflow when an analyst, data engineer, or growth operator needs tweets from an X search query in a JSON Lines handoff, analyst CSV file, XLSX workbook, warehouse load, CRM enrichment job, or queue. The CLI command below calls GET /x/tweets/search. It maps the same REST API query parameters to flags: --q, --limit, --cursor, --since-time, --until-time, and --query-type.
query="from:xquikcom webhook OR SDK"
cursor=""
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"]'

: > xquik-tweet-search.jsonl
jq -nr "$headers | @csv" > xquik-tweet-search.csv

while :; do
  page_cursor="$cursor"

  if [ -n "$cursor" ]; then
    x-twitter-scraper x:tweets search \
      --q "$query" \
      --query-type Latest \
      --cursor "$cursor" \
      --format json \
      --format-error json > search-page.json
  else
    x-twitter-scraper x:tweets search \
      --q "$query" \
      --query-type Latest \
      --limit 100 \
      --format json \
      --format-error json > search-page.json
  fi

  has_next_page="$(jq -r '.has_next_page // false' search-page.json)"
  next_cursor="$(jq -r 'if .has_next_page then (.next_cursor // "") else "" end' search-page.json)"

  jq -c \
    --arg source "xquik.cli.search" \
    --arg query "$query" \
    --arg page_cursor "$page_cursor" \
    --arg next_cursor "$next_cursor" \
    --argjson page_index "$page_index" \
    --argjson has_next_page "$has_next_page" \
    '.tweets[] | {
      source: $source,
      query: $query,
      tweet_id: .id,
      text: .text,
      author_id: (.author.id // ""),
      author_username: (.author.username // ""),
      author_name: (.author.name // ""),
      created_at: (.createdAt // ""),
      like_count: (.likeCount // 0),
      reply_count: (.replyCount // 0),
      retweet_count: (.retweetCount // 0),
      quote_count: (.quoteCount // 0),
      view_count: (.viewCount // 0),
      bookmark_count: (.bookmarkCount // 0),
      is_note_tweet: (.isNoteTweet // false),
      page_index: $page_index,
      page_cursor: $page_cursor,
      next_cursor: $next_cursor,
      has_next_page: $has_next_page
    }' search-page.json >> xquik-tweet-search.jsonl

  jq -r \
    --arg source "xquik.cli.search" \
    --arg query "$query" \
    --arg page_cursor "$page_cursor" \
    --arg next_cursor "$next_cursor" \
    --argjson page_index "$page_index" \
    --argjson has_next_page "$has_next_page" \
    '.tweets[] | [
      $source,
      $query,
      .id,
      .text,
      (.author.id // ""),
      (.author.username // ""),
      (.author.name // ""),
      (.createdAt // ""),
      (.likeCount // 0),
      (.replyCount // 0),
      (.retweetCount // 0),
      (.quoteCount // 0),
      (.viewCount // 0),
      (.bookmarkCount // 0),
      (.isNoteTweet // false),
      $page_index,
      $page_cursor,
      $next_cursor,
      $has_next_page
    ] | @csv' search-page.json >> xquik-tweet-search.csv

  [ "$has_next_page" = "true" ] || break
  [ -n "$next_cursor" ] || break
  cursor="$next_cursor"
  page_index=$((page_index + 1))
done
The response includes .tweets[], .has_next_page, and .next_cursor. Each tweet can include id, text, createdAt, engagement counts, bookmarkCount, isNoteTweet, author, media, quoted_tweet, and retweeted_tweet, depending on what X returns for the result. Project .tweets[] into xquik-tweet-search.jsonl and xquik-tweet-search.csv rows with tweet_id, author_username, engagement counts, page_index, page_cursor, next_cursor, and has_next_page so scripts can resume safely or load the same records into XLSX, CRM, warehouse, or agent workflows.
python3 - <<'PY'
import csv
import json

rows = [json.loads(line) for line in open("xquik-tweet-search.jsonl", encoding="utf-8")]
fieldnames = [
    "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",
]
with open("xquik-tweet-search-from-jsonl.csv", "w", newline="", encoding="utf-8") as handle:
    writer = csv.DictWriter(handle, fieldnames=fieldnames)
    writer.writeheader()
    writer.writerows(rows)
PY
Save the cursor with the job checkpoint before requesting the next page:
tail -n 1 xquik-tweet-search.jsonl | jq '{query, page_index, next_cursor, has_next_page}'
Use --limit for a bounded request from 1 to 200. Omit it when passing --cursor in page loops. 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. Use --format-error json so batch jobs can branch on status and code, and retry 429 or temporary 5xx responses with backoff. For XLSX handoff, keep xquik-tweet-search.jsonl as the source of truth and convert the same projected rows in your pipeline when account managers need a workbook.

Workflow: Follower Export to CSV, JSON, or XLSX

Use this workflow when sales, support, research, or marketing teams need an owned follower list in a CRM import, warehouse load, analyst CSV file, XLSX workbook, or resumable JSON handoff. follower_explorer requires targetUsername. The CLI flag is --target-username.
x-twitter-scraper extractions estimate-cost \
  --tool-type follower_explorer \
  --target-username xquikcom \
  --format json > follower-estimate.json
The estimate calls POST /extractions/estimate and returns allowed, estimatedResults, creditsRequired, creditsAvailable, and source. For follower exports, source is usually followers.
jq -e '.allowed == true' follower-estimate.json >/dev/null

job_id="$(x-twitter-scraper extractions run \
  --tool-type follower_explorer \
  --target-username xquikcom \
  --transform id \
  --raw-output)"
Persist job_id, the source username, and the estimate before polling so a shell restart, queue retry, or CI rerun can resume the same follower export.
while :; do
  x-twitter-scraper extractions retrieve \
    --id "$job_id" \
    --limit 1000 \
    --format json > followers-page.json

  status="$(jq -r '.job.status' followers-page.json)"
  [ "$status" = "completed" ] && break
  [ "$status" = "failed" ] && exit 1
  sleep 10
done
The retrieve command returns .job, .results, .hasMore, and .nextCursor. When .hasMore is true, pass .nextCursor back as --after to fetch the next saved page.
: > xquik-followers.jsonl
after=""

while :; do
  if [ -n "$after" ]; then
    x-twitter-scraper extractions retrieve \
      --id "$job_id" \
      --after "$after" \
      --limit 1000 \
      --format json > followers-page.json
  else
    x-twitter-scraper extractions retrieve \
      --id "$job_id" \
      --limit 1000 \
      --format json > followers-page.json
  fi

  jq -c '.results[]' followers-page.json >> xquik-followers.jsonl

  has_more="$(jq -r '.hasMore // false' followers-page.json)"
  next_cursor="$(
    jq -r 'if .hasMore then (.nextCursor // "") else "" end' followers-page.json
  )"
  [ "$has_more" = "true" ] || break
  [ -n "$next_cursor" ] || break
  after="$next_cursor"
done
x-twitter-scraper extractions export-results \
  --id "$job_id" \
  --format csv \
  --output xquik-followers.csv

x-twitter-scraper extractions export-results \
  --id "$job_id" \
  --format json \
  --output xquik-followers.json

x-twitter-scraper extractions export-results \
  --id "$job_id" \
  --format xlsx \
  --output xquik-followers.xlsx
Map User ID or result xUserId as the CRM unique key. Keep xquik-followers.jsonl for queue replay or warehouse loads, use 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 moderation, support, giveaway, research, or AI review job needs every reply under one tweet as a file handoff. The CLI calls the extraction endpoints: estimate first, run reply_extractor, poll the stored job, then export CSV, JSON, or XLSX. reply_extractor requires targetTweetId. The CLI flag is --target-tweet-id.
x-twitter-scraper extractions estimate-cost \
  --tool-type reply_extractor \
  --target-tweet-id 1893704267862470862 \
  --format json > reply-estimate.json
The estimate calls POST /extractions/estimate and returns allowed, estimatedResults, creditsRequired, creditsAvailable, and source. For reply scraping, source is usually replyCount.
jq -e '.allowed == true' reply-estimate.json >/dev/null

job_id="$(x-twitter-scraper extractions run \
  --tool-type reply_extractor \
  --target-tweet-id 1893704267862470862 \
  --transform id \
  --raw-output)"
The run command calls POST /extractions. Persist job_id before polling so a shell restart, queue retry, or CI rerun can resume the same extraction.
while :; do
  x-twitter-scraper extractions retrieve \
    --id "$job_id" \
    --limit 100 \
    --format json > replies-page.json

  status="$(jq -r '.job.status' replies-page.json)"
  [ "$status" = "completed" ] && break
  [ "$status" = "failed" ] && exit 1
  sleep 10
done
The retrieve command calls GET /extractions/{id}. The JSON response includes .job, .results, .hasMore, and .nextCursor. When .hasMore is true, save the cursor and call extractions retrieve again:
next_cursor="$(jq -r '.nextCursor // empty' replies-page.json)"

x-twitter-scraper extractions retrieve \
  --id "$job_id" \
  --after "$next_cursor" \
  --limit 100 \
  --format json > replies-next-page.json
Append .results[] to your queue, CRM import, or warehouse load.
x-twitter-scraper extractions export-results \
  --id "$job_id" \
  --format csv \
  --output xquik-tweet-replies.csv

x-twitter-scraper extractions export-results \
  --id "$job_id" \
  --format json \
  --output xquik-tweet-replies.json

x-twitter-scraper extractions export-results \
  --id "$job_id" \
  --format xlsx \
  --output xquik-tweet-replies.xlsx
The export command calls GET /extractions/{id}/export and writes the response bytes to --output. Use --format csv for analyst spreadsheets, --format json for pipelines, and --format xlsx for account-management workbooks. Cost: 1 credit per reply extracted or returned. Exports are free after the extraction job exists.

Workflow: Post Media Tweets, Replies, and DM Attachments

Use this workflow when an operator, support queue, or agent needs to publish a media-backed tweet, reply with media, or send one media attachment in a DM from a connected X account. Tweet and reply media posts use public media URLs directly on x:tweets create. Pass repeated --media flags for up to 4 image URLs, or pass exactly 1 MP4 video URL up to 100 MB. Do not mix video with other media. Do not upload first when you already have public media URLs.
x-twitter-scraper x:tweets create \
  --account @xquikcom \
  --text "New demo video is live." \
  --media https://example.com/product-demo.mp4 \
  --format json \
  --format-error json > posted-tweet.json
To reply with media, add the parent tweet ID. The --reply-to-tweet-id flag maps to reply_to_tweet_id, and --media maps to the public URL array on POST /x/tweets.
x-twitter-scraper x:tweets create \
  --account @xquikcom \
  --text "Here is the requested screenshot." \
  --reply-to-tweet-id 1893704267862470862 \
  --media https://example.com/export-preview.png \
  --format json \
  --format-error json > posted-reply.json
The response returns tweetId, success, charged, and chargedCredits. If the API returns 202 x_write_unconfirmed, store writeActionId, chargedCredits, and poll GET /x/write-actions/{id} before sending another write. Keep one JSON Lines handoff shape for both outcomes:
write_handoff() {
  input_file="$1"

  jq -c '
    if .error == "x_write_unconfirmed" then
      {
        status,
        write_action_id: .writeActionId,
        charged,
        charged_credits: .chargedCredits,
        retryable,
        poll: "GET /x/write-actions/{id}"
      }
    else
      {
        status: "posted",
        tweet_id: .tweetId,
        charged,
        charged_credits: .chargedCredits,
        write_action_id: .writeActionId
      }
    end
  ' "$input_file"
}

write_handoff posted-tweet.json > tweet-handoff.jsonl
write_handoff posted-reply.json > reply-handoff.jsonl
Each create-tweet call costs 10 credits. Use x:media upload when you need an uploaded media ID for a DM attachment. The CLI upload command accepts a local file through --file; --transform mediaId extracts the upload response ID for scripts.
media_id="$(x-twitter-scraper x:media upload \
  --account @xquikcom \
  --file ./handoff.png \
  --transform mediaId \
  --raw-output)"

x-twitter-scraper x:dm send \
  --account @xquikcom \
  --user-id 44196397 \
  --text "Here is the asset." \
  --media-id "$media_id" \
  --format json > sent-dm.json

jq -c --arg media_id "$media_id" '{
  status: "sent",
  message_id: .messageId,
  media_id: $media_id
}' sent-dm.json > dm-handoff.jsonl
The --media-id flag maps to the REST media_ids body field. DMs accept exactly 1 uploaded media ID. Store messageId from the DM response on the support ticket, CRM record, queue job, or agent memory. Uploading media costs 10 credits, and sending the DM costs 10 credits. Do not pass --reply-to-message-id to x:dm send; the REST endpoint rejects reply_to_message_id. Start a new DM with --user-id, --account, --text, and optional one-item --media-id instead. Do not pass uploaded mediaId values to x:tweets create; that command uses --media with public media URLs.

Useful Commands

Search tweets or scrape tweets

Run x-twitter-scraper x:tweets search to return tweet objects, author objects, metrics, media, has_next_page, and next_cursor.

Fetch one tweet

Run x-twitter-scraper x:tweets retrieve to return the full tweet text, author, metrics, media, quoted tweet, and retweeted tweet.

Get tweet replies

Run x-twitter-scraper x:tweets get-replies to return reply tweets plus cursor fields.

Export followers

Run x-twitter-scraper x:users retrieve-followers to return user profiles, follower counts, and cursor fields.

Post a tweet or reply

Run x-twitter-scraper x:tweets create to return tweetId, success, charged, and chargedCredits, or writeActionId when confirmation is pending.

Upload media for a DM attachment

Run x-twitter-scraper x:media upload to return mediaId for one-item DM media_ids and mediaUrl for tweet media URL handoff.

Send a direct message

Run x-twitter-scraper x:dm send to return messageId and success.
Use --format json for scripts, --format yaml for readable operations output, and --transform with GJSON syntax when you only need one field from the response.

Error Handling

The CLI writes API errors to stderr and exits non-zero for failed requests. Use --format-error json when scripts need machine-readable errors.
x-twitter-scraper x:users retrieve-search \
  --q not-a-real-user \
  --format json \
  --format-error json
Retryable API responses follow the same semantics as the REST API. See error handling and rate limits.

Pagination

Commands that call paginated endpoints include pagination fields in their JSON output. Keep the response body when your script needs to request the next page with endpoint cursor parameters.

Webhooks & References

Last modified on May 20, 2026