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

> 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 you need a TypeScript service to search tweets, scrape tweets to JSON Lines, CSV, or XLSX, export follower or profile data, post media tweets, upload media, send DMs, monitor tweets, or hand X data to another system through SDK calls.

## 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 it as the ISO 8601 lower time bound.
  </Card>

  <Card title="untilTime" icon="clock">
    Maps to REST `untilTime`. Use it as the ISO 8601 upper time bound.
  </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 after: string | undefined;
const jsonl = await open("xquik-followers.jsonl", "w");

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

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

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

    after = 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`. Persist `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 `after` 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/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 `after`. 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`, then 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, and `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.

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, and 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/get)
* [Export Extraction](/api-reference/extractions/export)
* [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)
* [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="file-warning">
    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`. Read calls are rate-limited, and `429` responses include `Retry-After`.

The client retries connection errors, 408, 409, 429, and 5xx responses by default. Handle `RateLimitError` with backoff, and 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)
* [REST API Overview](/api-reference/overview)
* [OpenAPI JSON](https://xquik.com/openapi.json)
* [Source Repository](https://github.com/Xquik-dev/x-twitter-scraper-typescript)
