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

# Scrape tweets by keyword to CSV, JSON, or XLSX

> Scrape tweets by keyword, run tweet_search_extractor, cap results, and export CSV/JSON/XLSX through the Xquik API.

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

Use this workflow when a research, sales, support, or monitoring system needs tweets as rows. Xquik can search tweets by keyword, hashtag, author, language, date range, media type, or engagement filters with `tweet_search_extractor`, estimate the cost first, run the extraction job, then export CSV, JSON, or XLSX.

## When to use this workflow

<CardGroup cols={2}>
  <Card title="Spreadsheet export" icon="file-spreadsheet">
    Use `tweet_search_extractor` plus CSV or XLSX export when analysts need rows.
  </Card>

  <Card title="App ingestion" icon="database">
    Use `tweet_search_extractor` plus paginated JSON results for queues, CRMs, or warehouses.
  </Card>

  <Card title="Latest page" icon="search">
    Use `GET /x/tweets/search` when you only need the newest matching API page.
  </Card>

  <Card title="Cost control" icon="gauge">
    Set `resultsLimit` on estimate and create requests before scraping.
  </Card>
</CardGroup>

## Choose the right path

<CardGroup cols={2}>
  <Card title="Saved tweet search export" icon="archive">
    Call `POST /extractions/estimate`, then `POST /extractions` when you need a reusable job ID, cost preview, CSV, JSON, or XLSX download.
  </Card>

  <Card title="Paginated extraction rows" icon="list-tree">
    Call `GET /extractions/{id}?limit=1000&after={nextCursor}` when a CRM, warehouse, queue, or agent needs saved JSON pages.
  </Card>

  <Card title="Live direct API page" icon="zap">
    Call `GET /x/tweets/search?q={query}` when you need cursor-driven pages, or one Tweet ID or X status URL lookup, without a stored extraction job.
  </Card>

  <Card title="Shell or SDK handoff" icon="terminal">
    Use [CLI](/sdks/cli), [TypeScript](/sdks/typescript), [Python](/sdks/python), or [Go](/sdks/go) when code should own JSON Lines, CSV, or XLSX files such as `xquik-tweet-search.jsonl`.
  </Card>
</CardGroup>

Use extraction jobs for repeatable exports and audit trails. Use the direct API for low-latency pages, small app handoffs, or exact lookup from one stored Tweet ID or X status URL.

<Info>
  Estimate before large jobs. The `202 Accepted` create response queues the job. It does not prove credits are reserved. Poll `GET /extractions/{id}` before fetching rows. The run can lower `resultsLimit` to the affordable result count. It can also finish as `failed` with `insufficient_credits`.
</Info>

## End-to-end export handoff

Store one checkpoint that carries the search request through estimate, job creation, JSON pagination, and file export:

```json theme={null}
{
  "workflow": "tweet_search_export",
  "request": {
    "toolType": "tweet_search_extractor",
    "searchQuery": "launch announcement",
    "language": "en",
    "sinceDate": "2026-05-01",
    "mediaType": "media",
    "resultsLimit": 500
  },
  "estimate": {
    "estimatedResults": 500,
    "creditsRequired": "500",
    "creditsAvailable": "77000",
    "allowed": true,
    "source": "resultsLimit"
  },
  "create_receipt": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "toolType": "tweet_search_extractor",
    "status": "running",
    "poll_path": "/api/v1/extractions/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  },
  "json_pages": {
    "limit": 1000,
    "page_cursor": null,
    "next_cursor": "990200",
    "has_more": true
  },
  "export_paths": {
    "csv": "/api/v1/extractions/a1b2c3d4-e5f6-7890-abcd-ef1234567890/export?format=csv",
    "json": "/api/v1/extractions/a1b2c3d4-e5f6-7890-abcd-ef1234567890/export?format=json",
    "xlsx": "/api/v1/extractions/a1b2c3d4-e5f6-7890-abcd-ef1234567890/export?format=xlsx"
  },
  "normalized_row": {
    "tweet_id": "1893710452812718080",
    "tweet_text": "We launched the new workflow today.",
    "tweet_created_at": "2026-05-01T10:05:00.000Z",
    "author_user_id": "44196397",
    "author_username": "username",
    "export_format": "csv"
  },
  "handoff_state": "poll_until_completed_then_export"
}
```

<CardGroup cols={2}>
  <Card title="Estimate checkpoint" icon="calculator">
    Keep `estimatedResults`, `creditsRequired`, `creditsAvailable`, `allowed`, and `source` with the original query and filters.
  </Card>

  <Card title="Job checkpoint" icon="clipboard-check">
    Store the returned job `id`, `status`, and `poll_path`; do not expect result rows in the create response.
  </Card>

  <Card title="Cursor checkpoint" icon="shuffle">
    Store `page_cursor`, `next_cursor`, and `has_more` for JSON page loops, then pass `nextCursor` back as `after`.
  </Card>

  <Card title="Export checkpoint" icon="download">
    Store the chosen CSV, JSON, or XLSX `export_paths` and the normalized row fields sent downstream.
  </Card>
</CardGroup>

## Data you get

Tweet search exports include base user fields, tweet fields, engagement counts, and metadata when available.

<CardGroup cols={2}>
  <Card title="Tweet author" icon="user-round">
    User ID, username, display name, follower count, verified state, and profile image.
  </Card>

  <Card title="Tweet" icon="message-square-text">
    Tweet ID, tweet text, tweet created time, and permalink.
  </Card>

  <Card title="Engagement" icon="chart-no-axes-combined">
    Likes, reposts, replies, quotes, views, and bookmarks.
  </Card>

  <Card title="Metadata" icon="file-json">
    Language, source app, conversation ID, and attached media.
  </Card>
</CardGroup>

## Step 1: Estimate tweets and credits

Call `POST /extractions/estimate` before scraping tweets. `tweet_search_extractor` requires `searchQuery`. Add `resultsLimit` when you want a sample or a hard cost cap.

```bash theme={null}
curl -X POST https://xquik.com/api/v1/extractions/estimate \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "toolType": "tweet_search_extractor",
    "searchQuery": "launch announcement",
    "language": "en",
    "sinceDate": "2026-05-01",
    "mediaType": "media",
    "resultsLimit": 500
  }' | jq
```

The estimate returns `allowed`, `estimatedResults`, `creditsRequired`, `creditsAvailable`, and `source`. For tweet search scraping, `source` is `resultsLimit` when a cap is set and `unknown` when no cap is set.

## Filter fields to operators

`tweet_search_extractor` merges structured fields into `searchQuery` before the job runs. Use fields when UI or SDK code owns filters; use `advancedQuery` when you already have a trusted X search operator string.

<CardGroup cols={2}>
  <Card title="Author and mentions" icon="at-sign">
    `fromUser` becomes `from:username`, `toUser` becomes `to:username`, and `mentioning` becomes `@username`.
  </Card>

  <Card title="Dates and language" icon="calendar-days">
    `language` becomes `lang:en`, `sinceDate` becomes `since:YYYY-MM-DD`, and `untilDate` becomes `until:YYYY-MM-DD`.
  </Card>

  <Card title="Media and engagement" icon="sliders-horizontal">
    `mediaType` accepts `images`, `videos`, `media`, or `gifs`; use `minFaves`, `minRetweets`, and `minReplies` for thresholds.
  </Card>

  <Card title="Replies and retweets" icon="git-branch">
    `replies` and `retweets` accept `exclude`, `include`, or `only`; `verifiedOnly` adds `filter:verified`.
  </Card>
</CardGroup>

`exactPhrase` quotes the value, `excludeWords` turns comma-separated words into `-word` filters, and `advancedQuery` is appended to the final query. For direct `GET /x/tweets/search`, put the same operators in `q`. A plain Tweet ID or X status URL in `q` is a direct lookup, not a saved extractor job.

## Step 2: Run the tweet search extraction

Create the job with the same `toolType`, `searchQuery`, filters, and optional `resultsLimit`.

```bash theme={null}
curl -X POST https://xquik.com/api/v1/extractions \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "toolType": "tweet_search_extractor",
    "searchQuery": "launch announcement",
    "language": "en",
    "sinceDate": "2026-05-01",
    "mediaType": "media",
    "resultsLimit": 500
  }' | jq
```

```json theme={null}
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "toolType": "tweet_search_extractor",
  "status": "running"
}
```

Store the creation response as a local handoff before polling:

```json theme={null}
{
  "job": "tweet_search_extraction",
  "tweet_search_extraction_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "search_query": "launch announcement",
  "filters": {
    "language": "en",
    "since_date": "2026-05-01",
    "media_type": "media"
  },
  "results_limit": 500,
  "status": "running",
  "handoff_created_at": "2026-05-16T03:25:00.000Z"
}
```

Poll by `tweet_search_extraction_id`; keep `search_query`, `filters`, and `results_limit` with the audit record. Do not wait for `totalResults` or `createdAt` in the create response; those fields arrive from `GET /extractions/{id}`.

## Step 3: Poll job status

Poll `GET /extractions/{id}` until the job is `completed` or `failed`.

```bash theme={null}
curl https://xquik.com/api/v1/extractions/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
  -H "x-api-key: xq_YOUR_KEY_HERE" | jq
```

Use the paginated response when your app wants JSON rows instead of a file download.

## Step 4: Export CSV, JSON, or XLSX

Exports are free after the extraction job exists. Use CSV for spreadsheets, JSON for app ingestion, and XLSX for analyst handoff. CSV, JSON, and XLSX exports are capped at 100,000 rows.

```bash theme={null}
curl -X GET "https://xquik.com/api/v1/extractions/a1b2c3d4-e5f6-7890-abcd-ef1234567890/export?format=csv" \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -o tweet-search.csv
```

```bash theme={null}
curl -X GET "https://xquik.com/api/v1/extractions/a1b2c3d4-e5f6-7890-abcd-ef1234567890/export?format=json" \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -o tweet-search.json
```

```bash theme={null}
curl -X GET "https://xquik.com/api/v1/extractions/a1b2c3d4-e5f6-7890-abcd-ef1234567890/export?format=xlsx" \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -o tweet-search.xlsx
```

## Step 5: Hand off rows

For API handoff, call `GET /extractions/{id}` with `limit` up to `1000` and pass `nextCursor` as `after` until `hasMore` is `false`. Store `job`, `results`, `hasMore`, and `nextCursor`.

Normalize each tweet row before sending it downstream:

```json theme={null}
{
  "job": "tweet_search_export",
  "search_query": "launch announcement",
  "result_id": "b2c3d4e5-f6a1-7890-abcd-ef2345678901",
  "tweet_id": "1893710452812718080",
  "tweet_text": "We launched the new workflow today.",
  "tweet_created_at": "2026-05-01T10:05:00.000Z",
  "author_user_id": "44196397",
  "author_username": "username",
  "source": "tweet_search_extractor",
  "export_format": "json"
}
```

For direct API handoff, normalize `tweets[].id`, `tweets[].text`, `tweets[].createdAt`, `tweets[].author.id`, `tweets[].author.username`, `has_next_page`, and `next_cursor`. For JSON Lines, write one normalized tweet object per line to `xquik-tweet-search.jsonl`.

## Direct tweet search API

Use `GET /x/tweets/search` when you need a paginated API response instead of a stored extraction job.

```bash theme={null}
curl -G https://xquik.com/api/v1/x/tweets/search \
  --data-urlencode "q=launch announcement" \
  -H "x-api-key: xq_YOUR_KEY_HERE" | jq
```

The direct search API returns `tweets`, `has_next_page`, and `next_cursor`.
Leave `limit` unset for a simple cursor-driven page loop, and pass
`next_cursor` back as `cursor`. Set `limit` when you want Xquik to collect up
to that count in one bounded request; if `has_next_page` is `true`, continue
with the same query, filters, `queryType`, and `limit`, and set `cursor` to
`next_cursor`. Cost is 1 credit per tweet returned.

For account date windows, `sinceTime` and `untilTime` append `since:` and
`until:` operators to the search query. A request such as
`q=from:username&sinceTime=2026-05-01&untilTime=2026-05-02` behaves like
`from:username since:2026-05-01 until:2026-05-02`. Use `queryType=Latest` for
chronological backfills. Add a keyword, hashtag, or other operator when you
need normal search ranking.

For bounded direct API requests with `limit`, a bare `q=from:username` and no
`sinceTime` or `untilTime` is treated as a user timeline pull. It returns a
single page with `has_next_page: false`. Add another search term or use
`fromUser` with a keyword when you need search pagination for that user.

## Failure handling

<CardGroup cols={2}>
  <Card title="400 missing query" icon="circle-alert">
    Send a non-empty `q` value for direct search, or `searchQuery` for `tweet_search_extractor`.
  </Card>

  <Card title="402 credits" icon="coins">
    Subscribe, add credits, lower direct `limit`, or lower extraction `resultsLimit`.
  </Card>

  <Card title="429 rate limit" icon="timer-reset">
    Wait for `retryAfter` or the `Retry-After` header before retrying.
  </Card>

  <Card title="424 or 502 unavailable" icon="rotate-ccw">
    Retry with exponential backoff and keep the same query, filters, cursor, or job ID.
  </Card>
</CardGroup>

## Handoff checklist

<CardGroup cols={2}>
  <Card title="Spreadsheet" icon="file-spreadsheet">
    Export `format=csv` or `format=xlsx`.
  </Card>

  <Card title="App ingestion" icon="database">
    Export `format=json`, paginate `GET /extractions/{id}`, or normalize direct `tweets[]` pages.
  </Card>

  <Card title="Queue or data lake" icon="send">
    Write normalized rows to `xquik-tweet-search.jsonl`.
  </Card>

  <Card title="Cost control" icon="gauge">
    Set `resultsLimit` on both estimate and create calls.
  </Card>

  <Card title="Live monitoring" icon="radio">
    Create an account or keyword monitor with signed webhooks.
  </Card>
</CardGroup>

<Note>
  **Related:** [Extraction workflow](/guides/extraction-workflow) · [Create extraction](/api-reference/extractions/create) · [Get extraction](/api-reference/extractions/get) · [Search tweets](/api-reference/x/search-tweets) · [SDKs](/sdks)
</Note>
