> ## 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 API CLI

> Use the Xquik CLI to search tweets, export JSON Lines, CSV, or XLSX, post media tweets, send DMs, monitor tweets, and script X API workflows from a terminal.

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

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

```bash theme={null}
go install github.com/Xquik-dev/x-twitter-scraper-cli/cmd/x-twitter-scraper@latest
```

Make sure your Go bin directory is on `PATH`:

```bash theme={null}
export PATH="$PATH:$(go env GOPATH)/bin"
```

## Authenticate

```bash theme={null}
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

```bash theme={null}
x-twitter-scraper x:tweets search \
  --q from:elonmusk \
  --limit 10 \
  --format json
```

Use `--help` at any level:

```bash theme={null}
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`.

```bash theme={null}
query="from:username 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" \
      --limit 100 \
      --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.

```bash theme={null}
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:

```bash theme={null}
tail -n 1 xquik-tweet-search.jsonl | jq '{query, page_index, next_cursor, has_next_page}'
```

Use `--limit` as a 1 to 200 upper bound for a bounded pull. When `.has_next_page` is true, keep the same `--q`, filters, `--query-type`, and `--limit`; only `--cursor` changes.

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`. For bounded pulls that return fewer tweets than the requested `--limit`, pass `.next_cursor` back as `--cursor` with the same query, filters, `--query-type`, and `--limit`. 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`.

```bash theme={null}
x-twitter-scraper extractions estimate-cost \
  --tool-type follower_explorer \
  --target-username username \
  --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`.

```bash theme={null}
jq -e '.allowed == true' follower-estimate.json >/dev/null

x-twitter-scraper extractions run \
  --tool-type follower_explorer \
  --target-username username \
  --format json > follower-run.json

jq -e '.status == "running" and .toolType == "follower_explorer"' follower-run.json >/dev/null
job_id="$(jq -r '.id' follower-run.json)"
```

The run command calls `POST /extractions` and returns the queued `202 Accepted` receipt: `id`, `toolType`, and `status: "running"`. Persist `follower-run.json`, `job_id`, the source username, and the estimate before polling. Credit reservation happens after the job starts. If available credits changed since the estimate, the run can fetch only the affordable count before export or mark the job `failed` with `insufficient_credits`.

```bash theme={null}
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.

```bash theme={null}
: > 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
```

```bash theme={null}
x-twitter-scraper extractions export-results \
  --id "$job_id" \
  --format csv \
  --output xquik-followers.csv
```

Use `--format json --output xquik-followers.json` for app ingestion or `--format xlsx --output xquik-followers.xlsx` for XLSX handoff. Map `User ID` or result `xUserId` as the CRM unique key. Keep `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 moderation, support, giveaway, research, or AI review needs every reply under one tweet as CSV, JSON, XLSX, or JSONL. Estimate, run `reply_extractor`, poll the stored job, then export the file.

`reply_extractor` requires `targetTweetId`. The CLI flag is `--target-tweet-id`.

```bash theme={null}
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 replies, `source` is usually `replyCount`.

```bash theme={null}
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. Reuse the follower export polling loop above with reply filenames:

```bash theme={null}
x-twitter-scraper extractions retrieve \
  --id "$job_id" \
  --limit 100 \
  --format json > replies-page.json

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
```

The retrieve command calls `GET /extractions/{id}` and returns `.job`, `.results`, `.hasMore`, and `.nextCursor`. Append `.results[]` to your queue, CRM import, or warehouse load. While `.hasMore` is true, pass `.nextCursor` back as `--after "$next_cursor"`.

```bash theme={null}
x-twitter-scraper extractions export-results \
  --id "$job_id" \
  --format csv \
  --output xquik-tweet-replies.csv
```

The export command calls `GET /extractions/{id}/export` and writes bytes to `--output`. Use `--format csv`, `--format json` with `--output xquik-tweet-replies.json`, or `--format xlsx` with `--output xquik-tweet-replies.xlsx`. 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.

```bash theme={null}
x-twitter-scraper x:tweets create \
  --account @username \
  --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`.

```bash theme={null}
x-twitter-scraper x:tweets create \
  --account @username \
  --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
```

Send a unique `Idempotency-Key`. Store the durable action's `id`, `request.hash`, `billing`, `result`, and `statusUrl`. Poll while `terminal` is false. Retry only when `safeToRetry` is true, using a new key. Keep one JSON Lines handoff shape for every lifecycle state:

```bash theme={null}
write_handoff() {
  input_file="$1"

  jq -c '
    {
      status,
      terminal,
      safe_to_retry: .safeToRetry,
      write_action_id: .id,
      request_hash: .request.hash,
      tweet_id: (.result.id // .tweetId // null),
      charged: .billing.charged,
      charged_credits: .billing.chargedCredits,
      poll: (if .terminal then null else .statusUrl end)
    }
  ' "$input_file"
}

write_handoff posted-tweet.json > tweet-handoff.jsonl
write_handoff posted-reply.json > reply-handoff.jsonl
```

Each create-tweet call costs 30 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.

```bash theme={null}
media_id="$(x-twitter-scraper x:media upload \
  --account @username \
  --file ./handoff.png \
  --transform mediaId \
  --raw-output)"

x-twitter-scraper x:dm send \
  --account @username \
  --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. Keep DM body text in private systems. Shared logs, public artifacts, queue status, and agent handoffs should store `message_id`, optional `media_id`, recipient/account identifiers from your job context, and send status instead of full DM bodies. 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

<CardGroup cols={2}>
  <Card title="Search tweets or scrape tweets" icon="search">
    Run `x-twitter-scraper x:tweets search` to return tweet objects, author objects, metrics, media, `has_next_page`, and `next_cursor`.
  </Card>

  <Card title="Fetch one tweet" icon="file-text">
    Run `x-twitter-scraper x:tweets retrieve` to return the full tweet text, author, metrics, media, quoted tweet, and retweeted tweet.
  </Card>

  <Card title="Get tweet replies" icon="message-square-text">
    Run `x-twitter-scraper x:tweets get-replies` to return reply tweets plus cursor fields.
  </Card>

  <Card title="Export followers" icon="users">
    Run `x-twitter-scraper x:users retrieve-followers` to return user profiles, follower counts, and cursor fields.
  </Card>

  <Card title="Post a tweet or reply" icon="send">
    Run `x-twitter-scraper x:tweets create` to return `tweetId`, `success`, `charged`, and `chargedCredits`, or `writeActionId` when confirmation is pending.
  </Card>

  <Card title="Upload media for a DM attachment" icon="image">
    Run `x-twitter-scraper x:media upload` to return `mediaId` for one-item DM `media_ids` and `mediaUrl` for tweet `media` URL handoff.
  </Card>

  <Card title="Send a direct message" icon="message-square">
    Run `x-twitter-scraper x:dm send` to return `messageId` and `success`.
  </Card>
</CardGroup>

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.

```bash theme={null}
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](/guides/error-handling) and [rate limits](/guides/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

* [Search Tweets](/api-reference/x/search-tweets)
* [Get User](/api-reference/x/get-user)
* [Create Tweet](/api-reference/x-write/create-tweet)
* [Upload Media](/api-reference/x-write/upload-media)
* [Send Direct Message](/api-reference/x-write/send-dm)
* [Webhook Overview](/webhooks/overview)
* [Verify HMAC Signatures](/webhooks/verification)
* [REST API Overview](/api-reference/overview)
* [Source Repository](https://github.com/Xquik-dev/x-twitter-scraper-cli)
