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

# TypeScript SDK for tweet search & X API automation

> Use the Xquik TypeScript SDK to search tweets, export JSON Lines, CSV, or XLSX, post media tweets, upload media, send DMs, and run Node.js X API workers.

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

Use the TypeScript SDK when you want typed request parameters, response models, retries, and autocomplete for Xquik REST API workflows in Node.js, Bun, or server-side TypeScript.

Use this page when a TypeScript service must search tweets or export them to JSON Lines, CSV, or XLSX. It can export followers and profiles, post media tweets, send DMs, monitor tweets, or send records downstream.

| TypeScript task | SDK call | Save |
| - | - | - |
| Search tweets | `client.x.tweets.search` | `nextCursor` |
| Export followers | `client.extractions.run` | `job.id` |

## Install

```bash theme={null}
npm install x-twitter-scraper
```

## Authenticate

```bash theme={null}
export X_TWITTER_SCRAPER_API_KEY="xq_YOUR_KEY_HERE"
```

```ts theme={null}
import XTwitterScraper from "x-twitter-scraper";

const client = new XTwitterScraper({
  apiKey: process.env["X_TWITTER_SCRAPER_API_KEY"],
});
```

## Basic example

Search tweets and write durable JSON Lines handoff rows:

```ts theme={null}
import XTwitterScraper from "x-twitter-scraper";

const client = new XTwitterScraper();

const page = await client.x.tweets.search({
  q: "from:username webhook OR SDK",
  limit: 10,
});

const tweetRows = page.tweets.map((tweet) => ({
  tweet_id: tweet.id,
  text: tweet.text,
  author_username: tweet.author?.username,
  created_at: tweet.createdAt,
}));

for (const row of tweetRows) {
  process.stdout.write(`${JSON.stringify(row)}\n`);
}
```

## Workflow: search tweets to JSON Lines, CSV, or XLSX

This job is for Node.js, Bun, and queue workers that need searchable tweet data in a stable handoff format for queues, data lakes, analyst CSV files, or XLSX workbooks. It calls `GET /x/tweets/search` through `client.x.tweets.search`, uses the generated `TweetSearchParams` shape, and writes each returned tweet as one projected JSON object per line.

```ts theme={null}
import XTwitterScraper from "x-twitter-scraper";

const client = new XTwitterScraper();
const query = "from:username webhook OR SDK";
let cursor: string | undefined;
let pageIndex = 0;

while (true) {
  const pageCursor = cursor ?? null;
  const page = await client.x.tweets.search({
    q: query,
    queryType: "Latest",
    cursor,
  });

  for (const tweet of page.tweets) {
    const row = {
      source: "xquik.typescript.search",
      query,
      tweet_id: tweet.id,
      text: tweet.text,
      author_id: tweet.author?.id ?? null,
      author_username: tweet.author?.username ?? null,
      author_name: tweet.author?.name ?? null,
      created_at: tweet.createdAt ?? null,
      like_count: tweet.likeCount ?? 0,
      reply_count: tweet.replyCount ?? 0,
      retweet_count: tweet.retweetCount ?? 0,
      quote_count: tweet.quoteCount ?? 0,
      view_count: tweet.viewCount ?? 0,
      bookmark_count: tweet.bookmarkCount ?? 0,
      is_note_tweet: tweet.isNoteTweet ?? false,
      page_index: pageIndex,
      page_cursor: pageCursor,
      next_cursor: page.next_cursor || null,
      has_next_page: page.has_next_page,
    };

    process.stdout.write(`${JSON.stringify(row)}\n`);
  }

  if (!page.has_next_page || !page.next_cursor) {
    break;
  }

  cursor = page.next_cursor;
  pageIndex += 1;
}
```

The generated params map directly to the REST endpoint:

<CardGroup cols={2}>
  <Card title="q" icon="search">
    Maps to REST `q`. Use it for the required X search query with keywords, handles, hashtags, or operators.
  </Card>

  <Card title="limit" icon="list">
    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, `queryType`, and `limit` when you continue with `page.next_cursor`.
  </Card>

  <Card title="cursor" icon="arrow-right">
    Maps to REST `cursor`. Pass the opaque cursor from `page.next_cursor` to request the next page.
  </Card>

  <Card title="sinceTime" icon="clock">
    Maps to REST `sinceTime`. Use the inclusive ISO bound on every page.
  </Card>

  <Card title="untilTime" icon="clock">
    Maps to REST `untilTime`. Use the exclusive ISO bound on every page.
  </Card>

  <Card title="queryType" icon="sliders-horizontal">
    Maps to REST `queryType`. Use `Latest` for chronological results or `Top` for engagement-ranked results.
  </Card>
</CardGroup>

## Returned data & handoff

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

<CardGroup cols={2}>
  <Card title="page.tweets" icon="message-square-text">
    JSON field `tweets`. Contains tweet records with `id`, `text`, optional `author`, `createdAt`, `likeCount`, `replyCount`, `retweetCount`, `quoteCount`, `bookmarkCount`, `viewCount`, and `isNoteTweet` when available.
  </Card>

  <Card title="page.has_next_page" icon="circle-check">
    TypeScript field `page.has_next_page`. JSON field `has_next_page`. Tells your worker whether another page exists.
  </Card>

  <Card title="page.next_cursor" icon="arrow-right">
    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` with the same query, filters, `queryType`, and `limit`.
  </Card>
</CardGroup>

Project `page.tweets` into JSON Lines rows in `xquik-tweet-search.jsonl` for queues and data lakes, transform the same projected records into CSV for analysts, or produce XLSX from those rows when account teams need spreadsheets. Store `tweet_id`, `author_username`, engagement counts, `page_index`, `page_cursor`, `next_cursor`, and `has_next_page` so a worker can resume from the last saved cursor without replaying raw SDK objects. For explicit `limit` pulls, resume with the same query, filters, `queryType`, and `limit`. Only `cursor` changes.

## Workflow: follower export to CSV, JSON, or XLSX

Use this workflow when a Node.js, Bun, or queue worker needs an owned follower list for CRM import, warehouse loading, account scoring, analyst CSV, XLSX workbook delivery, or a resumable JSON handoff. It calls `POST /extractions/estimate` through `client.extractions.estimateCost`, creates the job with `client.extractions.run`, reads saved rows with `client.extractions.retrieve`, and downloads files with `client.extractions.exportResults`.

<Info>
  `client.extractions.run` returns the queued `202 Accepted` receipt from `POST /extractions`: `id`, `toolType`, and `status: "running"`. Store `job.id` immediately, then poll `client.extractions.retrieve` before reading pages or calling `client.extractions.exportResults`. Credit reservation happens after the job starts. If available credits changed since `estimateCost`, the API can set `resultsLimit` to the affordable count before fetching rows or mark the job `failed` with `insufficient_credits`.
</Info>

```ts theme={null}
import { open, writeFile } from "node:fs/promises";
import XTwitterScraper from "x-twitter-scraper";

const client = new XTwitterScraper();
const targetUsername = "username";
const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));

const estimate = await client.extractions.estimateCost({
  toolType: "follower_explorer",
  targetUsername,
});

if (!estimate.allowed) {
  throw new Error("Insufficient credits for follower export.");
}

const job = await client.extractions.run({
  toolType: "follower_explorer",
  targetUsername,
});

while (true) {
  const statusPage = await client.extractions.retrieve(job.id, { limit: 1 });
  const status = statusPage.job["status"];

  if (status === "completed") {
    break;
  }

  if (status === "failed") {
    throw new Error("Follower export failed.");
  }

  await sleep(10000);
}

let cursor: string | undefined;
const jsonl = await open("xquik-followers.jsonl", "w");

try {
  while (true) {
    const page = await client.extractions.retrieve(job.id, {
      limit: 1000,
      cursor,
    });

    for (const row of page.results) {
      await jsonl.write(`${JSON.stringify(row)}\n`);
    }

    if (!page.hasMore || !page.nextCursor) {
      break;
    }

    cursor = page.nextCursor;
  }
} finally {
  await jsonl.close();
}

const csv = await client.extractions.exportResults(job.id, { format: "csv" });
await writeFile("xquik-followers.csv", Buffer.from(await csv.arrayBuffer()));

const json = await client.extractions.exportResults(job.id, { format: "json" });
await writeFile("xquik-followers.json", Buffer.from(await json.arrayBuffer()));

const xlsx = await client.extractions.exportResults(job.id, { format: "xlsx" });
await writeFile("xquik-followers.xlsx", Buffer.from(await xlsx.arrayBuffer()));
```

`follower_explorer` requires `targetUsername`. Store `job.id`, `targetUsername`, `estimate.estimatedResults`, and `estimate.source` before polling so a queue retry, worker restart, or agent handoff can resume the same export. `client.extractions.retrieve` returns `results`, `hasMore`, and `nextCursor`. Pass `nextCursor` back as `cursor` when you need stored JSON pages before exporting files. 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. Map exported `User ID` or row `xUserId` as the CRM unique key. 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 TypeScript worker needs every reply under one tweet as a saved extraction, JSON Lines handoff, or CSV, JSON, or XLSX export. It uses `client.extractions.estimateCost`, `run`, `retrieve`, and `exportResults`.

Reuse the polling, JSON Lines pagination, and export structure from the follower workflow. Only the tool type, target field, and filenames change:

```ts theme={null}
const targetTweetId = "1893704267862470862";

const estimate = await client.extractions.estimateCost({
  toolType: "reply_extractor",
  targetTweetId,
});

if (!estimate.allowed) {
  throw new Error("Insufficient credits for reply extraction.");
}

const job = await client.extractions.run({
  toolType: "reply_extractor",
  targetTweetId,
});

// Run the same polling and JSONL pagination loops from the follower workflow.
// Set the JSON Lines destination to await open("xquik-replies.jsonl", "w").

const csv = await client.extractions.exportResults(job.id, { format: "csv" });
await writeFile("xquik-replies.csv", Buffer.from(await csv.arrayBuffer()));

const json = await client.extractions.exportResults(job.id, { format: "json" });
await writeFile("xquik-replies.json", Buffer.from(await json.arrayBuffer()));

const xlsx = await client.extractions.exportResults(job.id, { format: "xlsx" });
await writeFile("xquik-replies.xlsx", Buffer.from(await xlsx.arrayBuffer()));
```

`reply_extractor` requires `targetTweetId`. `client.extractions.retrieve` returns `results`, `hasMore`, and `nextCursor`. The shared pagination loop passes `nextCursor` back as `cursor`. 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.exportResults` 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 TypeScript worker, support queue, 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 `client.x.tweets.create`. 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.

```ts theme={null}
interface TweetWriteAction {
  id: string;
  status: string;
  terminal: boolean;
  safeToRetry: boolean;
  statusUrl: string;
  request: { hash: string | null };
  billing: { charged: boolean; chargedCredits: string };
  result: { id?: string } | null;
  tweetId?: string;
}

function createTweetHandoff(
  action: TweetWriteAction,
  base: {
    account: string;
    media: string[];
    reply_to_tweet_id?: string;
  },
) {
  const thread = base.reply_to_tweet_id ? { reply_to_tweet_id: base.reply_to_tweet_id } : {};

  return {
    status: action.status,
    terminal: action.terminal,
    safe_to_retry: action.safeToRetry,
    write_action_id: action.id,
    request_hash: action.request.hash,
    tweet_id: action.result?.id ?? action.tweetId ?? null,
    charged: action.billing.charged,
    charged_credits: action.billing.chargedCredits,
    poll: action.terminal ? null : action.statusUrl,
    ...base,
    ...thread,
  };
}
```

Capture the durable write action from the raw response. Send a unique `Idempotency-Key`. Store `id`, `request.hash`, `billing`, `result`, and `statusUrl`. Poll while `terminal` is false. Retry only when `safeToRetry` is true, using a new key.

```ts theme={null}
const tweet = (await client.x.tweets.create({
  account: "@username",
  text: "New demo video is live.",
  media: ["https://example.com/product-demo.mp4"],
})) as TweetWriteAction;

const tweetHandoff = createTweetHandoff(tweet, {
  account: "@username",
  media: ["https://example.com/product-demo.mp4"],
});

process.stdout.write(`${JSON.stringify(tweetHandoff)}\n`);
```

To post an image reply, add `reply_to_tweet_id`:

```ts theme={null}
const reply = (await client.x.tweets.create({
  account: "@username",
  text: "Here is the requested screenshot.",
  reply_to_tweet_id: "1893704267862470862",
  media: ["https://example.com/export-preview.png"],
})) as TweetCreateResult;

const replyHandoff = createTweetHandoff(reply, {
  account: "@username",
  reply_to_tweet_id: "1893704267862470862",
  media: ["https://example.com/export-preview.png"],
});

process.stdout.write(`${JSON.stringify(replyHandoff)}\n`);
```

For DM attachments, upload the local file first and pass the returned `media.mediaId` as the only `media_ids` item:

```ts theme={null}
import fs from "node:fs";

const media = await client.x.media.upload({
  account: "@username",
  file: fs.createReadStream("./handoff.png"),
});

const dm = await client.x.dm.send("44196397", {
  account: "@username",
  text: "Here is the asset.",
  media_ids: [media.mediaId],
});

const dmHandoff = {
  status: "sent",
  message_id: dm.messageId,
  media_id: media.mediaId,
  account: "@username",
  user_id: "44196397",
};

process.stdout.write(`${JSON.stringify(dmHandoff)}\n`);
```

`client.x.tweets.create` returns `tweet.tweetId` for confirmed posts or the pending write fields above when confirmation is still running. `client.x.media.upload` returns `media.mediaId` for DM attachments. `client.x.dm.send` returns `dm.messageId` for support tickets, CRM records, queue jobs, 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`, `account`, `user_id`, and send status instead of full DM bodies. Leave `reply_to_message_id` unset even if generated SDK types expose it. The REST endpoint rejects DM reply threading.

`client.x.dm.deleteMessage` [deletes a DM](/api-reference/x-write/delete-dm).

Text-only tweet and reply writes cost 30 credits. Tweet media adds 2 credits per started MB across attached files. Uploading media costs 10 credits. Sending the DM costs 10 credits. Do not pass uploaded `media.mediaId` values to `client.x.tweets.create`. That method uses `media` with public media URLs.

Useful endpoints:

* [Search Tweets](/api-reference/x/search-tweets)
* [Create Extraction](/api-reference/extractions/create)
* [Get Extraction](/api-reference/extractions/twitter-extraction-results)
* [Export Extraction](/api-reference/extractions/export)
* [Get User](/api-reference/x/twitter-profile-lookup)
* [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)
* [Create Monitor](/api-reference/monitors/create)
* [Create Webhook](/api-reference/webhooks/create)

## Error handling

The SDK throws typed errors for API failures:

<CardGroup cols={2}>
  <Card title="400 Bad request" icon="triangle-alert">
    Throws `BadRequestError`.
  </Card>

  <Card title="401 Unauthenticated" icon="lock">
    Throws `AuthenticationError`.
  </Card>

  <Card title="403 Permission denied" icon="shield">
    Throws `PermissionDeniedError`.
  </Card>

  <Card title="404 Not found" icon="circle-x">
    Throws `NotFoundError`.
  </Card>

  <Card title="422 Unprocessable entity" icon="triangle-alert">
    Throws `UnprocessableEntityError`.
  </Card>

  <Card title="429 Rate limited" icon="gauge">
    Throws `RateLimitError`.
  </Card>

  <Card title="5xx server error" icon="server">
    Throws `InternalServerError`.
  </Card>
</CardGroup>

```ts theme={null}
import XTwitterScraper from "x-twitter-scraper";

const client = new XTwitterScraper();

try {
  await client.account.retrieve();
} catch (error) {
  if (error instanceof XTwitterScraper.APIError) {
    process.stderr.write(`HTTP ${error.status}\n`);
  } else {
    process.stderr.write("Network or timeout error\n");
  }
}
```

The client retries connection errors, 408, 409, 429, and 5xx responses by default. Set `maxRetries` to tune retry behavior.

## 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`. Xquik rate-limits read calls. `429` responses include `Retry-After`.

The client retries connection errors, 408, 409, 429, and 5xx responses by default. Handle `RateLimitError` with backoff. Fix 400, 401, 403, 404, or 422 responses before retrying.

## Pagination

Search and list endpoints return page objects. Check `has_next_page` and pass the generated cursor fields documented on each endpoint when you need another page.

```ts theme={null}
const firstPage = await client.x.tweets.search({ q: "xquik", limit: 20 });

if (firstPage.has_next_page) {
  process.stderr.write("More results are available\n");
}
```

## Webhooks & references

* [Webhook Overview](/webhooks/overview)
* [Verify HMAC Signatures](/webhooks/verification)
* [Twitter Scraper API Overview](/api-reference/overview)
* Download the OpenAPI schema: `curl -o openapi.json https://xquik.com/openapi.json`
* [Source Repository](https://github.com/Xquik-dev/x-twitter-scraper-typescript)


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