Skip to main content
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

npm install x-twitter-scraper

Authenticate

export X_TWITTER_SCRAPER_API_KEY="xq_YOUR_KEY_HERE"
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:
import XTwitterScraper from "x-twitter-scraper";

const client = new XTwitterScraper();

const page = await client.x.tweets.search({
  q: "from:xquikcom 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.
import XTwitterScraper from "x-twitter-scraper";

const client = new XTwitterScraper();
const query = "from:xquikcom 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:

q

Maps to REST q. Use it for the required X search query with keywords, handles, hashtags, or operators.

limit

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.

cursor

Maps to REST cursor. Pass the opaque cursor from page.next_cursor to request the next page.

sinceTime

Maps to REST sinceTime. Use it as the ISO 8601 lower time bound.

untilTime

Maps to REST untilTime. Use it as the ISO 8601 upper time bound.

queryType

Maps to REST queryType. Use Latest for chronological results or Top for engagement-ranked results.

Returned Data & Handoff

client.x.tweets.search returns PaginatedTweets:

page.tweets

JSON field tweets. Contains tweet records with id, text, optional author, createdAt, likeCount, replyCount, retweetCount, quoteCount, bookmarkCount, viewCount, and isNoteTweet when available.

page.has_next_page

TypeScript field page.has_next_page. JSON field has_next_page. Tells your worker whether another page exists.

page.next_cursor

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.
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.
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.
import { open, writeFile } from "node:fs/promises";
import XTwitterScraper from "x-twitter-scraper";

const client = new XTwitterScraper();
const targetUsername = "xquikcom";
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:
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.
type TweetCreateResult =
  | {
      success: true;
      tweetId: string;
      charged: true;
      chargedCredits: string;
      writeActionId?: string;
    }
  | {
      error: "x_write_unconfirmed";
      status: "pending_confirmation";
      writeActionId: string;
      charged: false;
      chargedCredits: "0";
      retryable: false;
    };

function createTweetHandoff(
  result: TweetCreateResult,
  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 }
    : {};

  if ("writeActionId" in result) {
    return {
      status: "pending_confirmation",
      write_action_id: result.writeActionId,
      charged: result.charged,
      charged_credits: result.chargedCredits,
      retryable: result.retryable,
      poll: "GET /x/write-actions/{id}",
      ...base,
      ...thread,
    };
  }

  const writeAction = result.writeActionId
    ? { write_action_id: result.writeActionId }
    : {};

  return {
    status: "posted",
    tweet_id: result.tweetId,
    charged: result.charged,
    charged_credits: result.chargedCredits,
    ...writeAction,
    ...base,
    ...thread,
  };
}
The generated TypeScript SDK models confirmed tweetId, charged, and chargedCredits responses. The REST API can also return 202 x_write_unconfirmed; treat that runtime value as pending, store writeActionId and chargedCredits, and poll Get Write Action Status before sending another write.
const tweet = (await client.x.tweets.create({
  account: "@xquikcom",
  text: "New demo video is live.",
  media: ["https://example.com/product-demo.mp4"],
})) as TweetCreateResult;

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

process.stdout.write(`${JSON.stringify(tweetHandoff)}\n`);
To post an image reply, add reply_to_tweet_id:
const reply = (await client.x.tweets.create({
  account: "@xquikcom",
  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: "@xquikcom",
  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:
import fs from "node:fs";

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

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

const dmHandoff = {
  status: "sent",
  message_id: dm.messageId,
  media_id: media.mediaId,
  account: "@xquikcom",
  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 10 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:

Error Handling

The SDK throws typed errors for API failures:

400 Bad Request

Throws BadRequestError.

401 Unauthenticated

Throws AuthenticationError.

403 Permission Denied

Throws PermissionDeniedError.

404 Not Found

Throws NotFoundError.

422 Unprocessable Entity

Throws UnprocessableEntityError.

429 Rate Limited

Throws RateLimitError.

5xx Server Error

Throws InternalServerError.
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.
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

Last modified on May 25, 2026