Skip to content

Twitter API in Node.js: a 2026 guide with working fetch code

Node.js reads Twitter/X data with its built-in fetch: send an HTTPS request with an API key in a header, parse the JSON, and pass the returned cursor back for the next page. The official X API charges $0.005 per post read.

Short answer: Node.js needs no package to read X data: fetch is built in. The official X API, even through twitter-api-v2, needs a developer account and prepaid credits: $0.005 per post, $0.015 with its author from November 22, 2026. For public data, use Sorsa API: one ApiKey header, 20 tweets per request from $0.07 per 1,000; 100 free requests fetch 2,000.

Endpoint
POST /search-tweets
Page size
20 tweets per request
Price
from $0.07 per 1,000 tweets

#Which npm package works with the official X API in 2026?

Two packages call the official X API from Node.js. twitter-api-v2, a community client, reached version 1.31.0 on October 25, 2026 and is installed about 336K times a week. X's own TypeScript SDK, @xdevplatform/xdk, is at 0.6.6. Both wrap the same paid platform, so neither works without a developer account, a bearer token and prepaid credits. The same requests in Python, with Tweepy and xdk in that role, are in the guide to the Twitter API in Python.

The credits are the part older Node tutorials skip. X has no free tier for new developers and bills every object a response returns: $0.005 per post and $0.010 per user, up to 3M post reads a month (X's price table (opens in a new tab)). From November 22, 2026, authors and media pulled in through expansions cost the same as direct reads (X's notice of October 23 (opens in a new tab)). A typical twitter-api-v2 search then pays for every author it attaches:

twitter-api-v2
import { TwitterApi } from "twitter-api-v2";

const client = new TwitterApi("YOUR_BEARER_TOKEN").readOnly;
const page = await client.v2.search("nodejs lang:en", {
  max_results: 100,
  expansions: ["author_id"],
});
// 100 posts: $0.50. From Nov 22, 2026, with authors: up to $1.50

Its paginators add a second trap: the library's own docs say a for await loop over a paginator keeps fetching until the results or your rate limit run out, and every page is billed. Every rate, cap and date is laid out in X API pricing.

What the official route needs before the first call

  • A developer account at console.x.com, opened with an X account that accepts X's Developer Agreement.
  • An app, whose bearer token X shows only once.
  • Credits bought in advance and spent per post, user and media object returned.
  • OAuth user tokens for anything that posts, likes or follows.

Reading public posts and profiles needs none of that. An X data API with its own key answers plain HTTPS, and Node's fetch is the whole client.

#Get tweets in Node.js with one API key

To read X data from Node.js, use Sorsa API, a Twitter (X) API and scraper: one ApiKey header instead of OAuth, 20 requests per second on every plan, free accounts included, and search results from $0.07 per 1,000 tweets. Every tweet arrives with its author's full profile, so code sets no expansions or field lists. Sorsa API never asks for an X login, so no X account of yours carries any risk.

Set up the key and send the first request

  1. Get a key. Sign up and copy your API key from the dashboard. The first 100 requests are free, need no card and never expire.

  2. Check Node and store the key. Run node -v: the LTS lines in November 2026 are 22, 24 and 26, and all three have fetch and --env-file built in. Put the key in a .env file that git ignores, and call the API only from server code.

    Terminal
    node -v                                  # v22 or newer
    echo 'SORSA_API_KEY=your_key' > .env
    echo '.env' >> .gitignore
    
  3. Search for tweets. Save this as first.mjs and run node --env-file=.env first.mjs. It sends a query in X's search syntax to POST /search-tweets (opens in a new tab) with the key in the ApiKey header; the .mjs extension turns on top-level await.

    const API = "https://api.sorsa.io/v3";
    const KEY = process.env.SORSA_API_KEY ?? "YOUR_API_KEY";
    
    const res = await fetch(`${API}/search-tweets`, {
      method: "POST",
      headers: { ApiKey: KEY, "Content-Type": "application/json" },
      body: JSON.stringify({ query: "nodejs lang:en", order: "latest" }),
    });
    if (!res.ok) throw new Error(`${res.status}: ${(await res.json()).message}`);
    
    const { tweets, next_cursor } = await res.json();
    for (const t of tweets) {
      console.log(t.user.username, t.full_text.slice(0, 60));
    }
    console.log("next page:", next_cursor);
    
  4. Read the page. Each response holds up to 20 tweets and a next_cursor for the next page. The values below are demo data in the documented shape.

    Response200 OK
    {
      "tweets": [
        {
          "id": "1985432176540981234",
          "full_text": "Moved our nightly X report to one Node script.",
          "created_at": "2026-11-01T18:42:07Z",
          "lang": "en",
          "likes_count": 58,
          "retweet_count": 9,
          "reply_count": 4,
          "quote_count": 1,
          "view_count": 4920,
          "bookmark_count": 6,
          "is_reply": false,
          "user": {
            "id": "1592041833471827968",
            "username": "node_stream_digest",
            "display_name": "Node Stream Digest",
            "followers_count": 2310,
            "verified": false
          }
        }
      ],
      "next_cursor": "DAABCgABF7Y8kQ3wAAA"
    }
    

Every field below comes straight from the response; the JavaScript type is what res.json() gives you, and IDs stay strings on purpose.

FieldJavaScript typeWhat it holds
idstringThe tweet's 19-digit ID: never convert it to a Number
full_textstringThe whole text of the post
created_atstringISO 8601 in UTC: new Date(t.created_at) parses it
likes_count, retweet_count, reply_count, quote_countnumberEngagement counts
view_count, bookmark_countnumberViews and bookmarks
userobjectThe author's profile: 19 fields such as username and followers_count
quoted_status, retweeted_statusobject or absentThe quoted or reposted tweet, in the same shape
  • id

    JavaScript type
    string
    What it holds
    The tweet's 19-digit ID: never convert it to a Number
  • full_text

    JavaScript type
    string
    What it holds
    The whole text of the post
  • created_at

    JavaScript type
    string
    What it holds
    ISO 8601 in UTC: new Date(t.created_at) parses it
  • likes_count, retweet_count, reply_count, quote_count

    JavaScript type
    number
    What it holds
    Engagement counts
  • view_count, bookmark_count

    JavaScript type
    number
    What it holds
    Views and bookmarks
  • user

    JavaScript type
    object
    What it holds
    The author's profile: 19 fields such as username and followers_count
  • quoted_status, retweeted_status

    JavaScript type
    object or absent
    What it holds
    The quoted or reposted tweet, in the same shape

#Retry 429s and server errors in one helper

Put every call behind one function that retries only what is worth retrying. A 429 means more than 20 requests started within one second on your key; a 5xx or a timeout is a passing fault. The rest of this guide imports this sorsa helper.

sorsa.mjs
// sorsa.mjs: one place for the key, timeouts and retries
const API = "https://api.sorsa.io/v3";
const KEY = process.env.SORSA_API_KEY ?? "YOUR_API_KEY";
const RETRY = new Set([429, 500, 502, 503, 504]);
const sleep = (ms) => new Promise((ok) => setTimeout(ok, ms));

export async function sorsa(path, { body, query } = {}) {
  const url = new URL(API + path);
  if (query) url.search = query; // a URLSearchParams for GET endpoints

  for (let attempt = 0; ; attempt++) {
    let res;
    try {
      res = await fetch(url, {
        method: body ? "POST" : "GET",
        headers: { ApiKey: KEY, "Content-Type": "application/json" },
        body: body && JSON.stringify(body),
        signal: AbortSignal.timeout(30_000),
      });
    } catch (err) { // timeout or dropped connection
      if (attempt === 4) throw err;
      await sleep(1000 * 2 ** attempt);
      continue;
    }
    if (res.ok) return res.json();
    if (!RETRY.has(res.status) || attempt === 4) {
      const { message } = await res.json().catch(() => ({}));
      throw new Error(`${res.status} on ${path}: ${message}`);
    }
    await sleep(1000 * 2 ** attempt); // 1, 2, 4, 8 seconds
  }
}

A 401 or 403 never earns a retry: the key is wrong or missing, or its requests are used up, and the message field says which. Failed requests are never charged, so a retry costs nothing. Every Sorsa API endpoint only reads, so repeating a POST such as /search-tweets can't create anything twice. Sorsa API sends no rate-limit headers because the window resets every second: the first one-second wait clears a 429.

#Page through results with next_cursor

A cursor is an opaque key to the next page: send next_cursor back exactly as it came and stop when a response has none. An async generator fits this loop well, because the code that reads the pages never sees the cursor at all.

pages.mjs
import { sorsa } from "./sorsa.mjs";

export async function* pages(path, body, maxPages = 100) {
  for (let n = 0; n < maxPages; n++) {
    const page = await sorsa(path, { body });
    yield page;
    if (!page.next_cursor) return; // null, empty or absent: the end
    body = { ...body, next_cursor: page.next_cursor };
  }
}
search.mjs
import { pages } from "./pages.mjs";

const search = { query: "nodejs lang:en", order: "latest" };
for await (const page of pages("/search-tweets", search, 50)) {
  for (const t of page.tweets) console.log(t.id, t.created_at);
}
Your code for await yield page 1 yield page 2 return query query + "c1" query + "c2" request page page 1 page 2 page 3 Sorsa API POST /search-tweets 20 tweets next_cursor: "c1" 17 tweets next_cursor: "c2" 12 tweets next_cursor: null
The generator sends each cursor back and stops on null; a short page in the middle is not the end.

Search and timelines return up to 20 tweets a page, and a page can hold fewer while more remain, so the count never tells you the end. Keep the query and order unchanged along one cursor, and treat maxPages as a budget: 50 pages are 50 requests, whatever the query matches. GET endpoints take the cursor in the query string instead: GET /followers (opens in a new tab) returns up to 200 profiles a page, and query.set("next_cursor", page.next_cursor) moves it on. POST /user-tweets (opens in a new tab) works with the same generator; to download all tweets from a user with a resumable cursor, that guide has the full script.

#Fetch 100 tweets or profiles per request

When you already hold IDs or usernames, send them in batches. POST /tweet-info-bulk (opens in a new tab) takes up to 100 tweet IDs or links in tweet_links, and GET /info-batch (opens in a new tab) takes up to 100 usernames; each batch is one request.

batch.mjs
import { sorsa } from "./sorsa.mjs";
// tweetIds and usernames: arrays of strings you already hold

const chunks = (list, size = 100) =>
  Array.from({ length: Math.ceil(list.length / size) },
    (_, i) => list.slice(i * size, (i + 1) * size));

const tweets = [];
for (const ids of chunks(tweetIds)) {
  const body = { tweet_links: ids }; // up to 100 IDs or links
  tweets.push(...(await sorsa("/tweet-info-bulk", { body })).tweets);
}

const users = [];
for (const names of chunks(usernames)) {
  const query = new URLSearchParams();
  for (const name of names) query.append("usernames", name);
  users.push(...(await sorsa("/info-batch", { query })).users);
}

GET /info-batch reads usernames as a repeated parameter (usernames=a&usernames=b), so build it with append. Passing an array to new URLSearchParams({ usernames }) joins the names with commas into one value instead. A request costs the same whatever it returns, so a batch of 100 tweets costs what one tweet costs: from $0.014 per 1,000 tweets, more than 250x less than the official X API's $5.00. Batch responses carry no cursor; the whole batch is in tweets or users.

#Run 20 requests per second from one key

Sorsa API allows 20 requests per second per key on every plan, and one Node process reaches that easily: the event loop keeps dozens of requests in flight on a single thread. The limit to respect is the start rate, so put one pacer in front of every call and split the work into independent chains. One cursor chain can't run in parallel, because each page needs the cursor of the page before, so run one chain per account or one per day of a search.

lanes.mjs
import { sorsa } from "./sorsa.mjs";

// Hands out start times 1/19 s apart, however many lanes are waiting.
function pacer(perSecond) {
  const gap = 1000 / perSecond;
  let next = 0;
  return () => {
    const now = Date.now();
    const start = Math.max(next, now);
    next = start + gap;
    return new Promise((ok) => setTimeout(ok, start - now));
  };
}

const slot = pacer(19); // one pacer per key, one start under the limit

async function lane(query, maxPages = 100) {
  const tweets = [];
  let body = { query };
  for (let n = 0; n < maxPages; n++) {
    await slot();
    const page = await sorsa("/search-tweets", { body });
    tweets.push(...page.tweets);
    if (!page.next_cursor) break;
    body = { ...body, next_cursor: page.next_cursor };
  }
  return tweets;
}

const day = (d) => new Date(Date.UTC(2026, 9, d)).toISOString().slice(0, 10);
const queries = Array.from({ length: 31 }, (_, i) =>
  `nodejs lang:en since:${day(i + 1)} until:${day(i + 2)}`); // Oct 1-31
const results = await Promise.all(queries.map((q) => lane(q)));
console.log(results.flat().length, "tweets");

The pacer needs no lock: JavaScript runs one callback at a time, so two lanes can never take the same start time. At 19 starts a second it leaves one start of headroom, because timer jitter can push the 20th start into the next window. The helper's retry catches any 429 that still slips through. Promise.all without a pacer is the common mistake: 60 calls fired at once put 40 straight into 429s and spend seconds in back-off.

One Node.js process pacer(19) Oct 1 Oct 2 Oct 3 Oct 4 one API key 19 starts a second under the limit of 20 per key Three serverless instances pacer(19) pacer(19) pacer(19) each counts only its own starts the same API key 57 starts a second 37 of them get a 429 and wait
A pacer only counts the starts of its own process, so give each key one pacer or split the rate between instances.

Serverless platforms break that rule quietly. Every function instance on Lambda, Vercel or Cloud Run loads its own copy of lanes.mjs. Three warm instances therefore run three pacers on one key and start up to 57 requests a second between them. Give each instance a share of the rate (pacer(6) for three), or run the bulk job in one worker. The limit is counted per key, so a second key for a second worker brings its own 20 a second. At 20 requests a second, batches of 100 move up to 2,000 tweets a second and search pages 400.

#Save tweets to JSONL or CSV

Write each page as it arrives instead of holding every tweet in memory. JSON Lines, one JSON object per line, keeps every field and appends safely; CSV opens in Excel and Google Sheets. Both streams below wait for drain when the disk falls behind.

save.mjs
import { createWriteStream } from "node:fs";
import { once } from "node:events";
import { pages } from "./pages.mjs";

const jsonl = createWriteStream("tweets.jsonl");
const csv = createWriteStream("tweets.csv");
const put = async (out, line) => {
  if (!out.write(line)) await once(out, "drain"); // respect backpressure
};
const cell = (v) => `"${String(v ?? "").replaceAll('"', '""')}"`;
const BOM = String.fromCharCode(0xfeff); // tells Excel the file is UTF-8

await put(csv, BOM + "id,created_at,username,likes_count,full_text\r\n");
const search = { query: "nodejs lang:en" };
for await (const page of pages("/search-tweets", search, 500)) {
  for (const t of page.tweets) {
    await put(jsonl, JSON.stringify(t) + "\n");
    const row = [t.id, t.created_at, t.user.username, t.likes_count,
      t.full_text];
    await put(csv, row.map(cell).join(",") + "\r\n");
  }
}
jsonl.end();
csv.end();

The byte order mark at the start of the CSV tells Excel the file is UTF-8, so emoji and accents show correctly. Every cell is quoted, so commas, quotes and line breaks inside a tweet stay in their column. Five hundred pages make up to 10K tweets for 500 requests; memory stays flat however long the run is.

Good to know

JavaScript numbers hold integers exactly only up to 2^53, about 9 quadrillion, and tweet IDs have 19 digits. Number("1985432176540981234") returns 1985432176540981200, a different tweet. Sorsa API sends every ID and cursor as a string, so keep them strings, compare them as strings, and use BigInt(id) when you need to sort by ID.

#Add TypeScript types for the responses

TypeScript users get type safety from a few interfaces named exactly as the API's fields. The ones below follow the live OpenAPI reference and cover the fields most code reads; the reference lists the rest.

sorsa-types.ts
// sorsa-types.ts: field names as in the Sorsa API reference
export interface SorsaUser {
  id: string; // 64-bit ID: keep it a string
  username: string;
  display_name: string;
  description: string;
  followers_count: number;
  followings_count: number;
  tweets_count: number;
  verified: boolean;
  protected: boolean;
  created_at: string; // ISO 8601
}

export interface SorsaTweet {
  id: string;
  full_text: string;
  created_at: string;
  lang: string;
  likes_count: number;
  retweet_count: number;
  reply_count: number;
  quote_count: number;
  view_count: number;
  bookmark_count: number;
  is_reply: boolean;
  user: SorsaUser;
  quoted_status?: SorsaTweet;
  retweeted_status?: SorsaTweet;
}

export interface TweetsPage {
  tweets: SorsaTweet[];
  next_cursor?: string | null;
}

Optional fields carry a ?: a quoted or reposted tweet exists only on quotes and reposts, so the compiler makes you check before you read it. Cast at the boundary, const page = (await sorsa(path, { body })) as TweetsPage, and the rest of the code is checked. Node 22.18 and later run .ts files directly by stripping the types, so node --env-file=.env job.ts works without a build step. For every endpoint at once, an OpenAPI generator such as openapi-typescript can turn the full reference into types.

#Can you call a Twitter API from browser JavaScript?

No: an API key in browser code is readable by anyone who opens the developer tools, and Sorsa API is built for server-side calls, so browsers get CORS errors. Put a small route on your server, keep the key there, and let the front end call your route.

// server.mjs: npm install express
import express from "express";
import { sorsa } from "./sorsa.mjs";

const app = express();
app.get("/api/tweets", async (req, res) => {
  const query = String(req.query.q ?? "").slice(0, 200);
  const page = await sorsa("/search-tweets", { body: { query } });
  res.json(page.tweets.map((t) => ({ id: t.id, text: t.full_text })));
});
app.listen(3000);

The browser then runs fetch("/api/tweets?q=nodejs") against your own domain and never sees the key. The same pattern fits any server runtime: a Cloudflare Worker or a Lambda function keeps the key in its environment variables, and the page only ever talks to it. Cache the route's answer for a minute when many visitors ask the same thing: Sorsa API returns fresh data on every request, and one cached answer serves them all for one request.

#What a Node.js job costs

Sorsa API charges per request, whatever the endpoint returns: search results cost from $0.07 per 1,000 tweets, batches from $0.014, and failed requests are never charged. The official X API charges per object instead: $5.00 per 1,000 posts, and from November 22, 2026 up to $15.00 per 1,000 when the code expands each post's author, as most twitter-api-v2 searches do.

Tweets your code collects by search

Official X API

$5,000

for 1M tweets

$0.005 per post

Sorsa API

from$70

for 1M tweets

from $0.07 per 1,000

Each tweet carries its author's profile

The builder below writes the Node.js loop for a job and shows its requests, the fastest time on one key and the cost at Sorsa API's lowest rate.

Job
How many
job.mjs
// sorsa() and pages() come from the helpers above
const body = { query: "nodejs lang:en" };
for await (const page of pages("/search-tweets", body, 5000)) {
  save(page.tweets); // up to 20 tweets a page
}
Requests
5,000
Fastest on one key
about 4 min
Sorsa API
from $7

Time at 20 requests a second; cost at $1.40 per 1,000 requests. One search is one cursor chain, so split it into date windows to run lanes side by side.

The time is the fastest one key allows; a single cursor chain runs at the pace of its round trips, so split big jobs into lanes.

The rate per 1,000 requests runs from $4.90 on Starter to $1.40 on Hyperscale, and every endpoint uses the same rate. Checking what is left costs nothing: GET /key-usage-info (opens in a new tab) never counts against your requests.

usage.mjs
import { sorsa } from "./sorsa.mjs";
const usage = await sorsa("/key-usage-info");
console.log(usage.remaining_requests, "left until", usage.valid_until);

#Moving twitter-api-v2 code to Sorsa API

Most twitter-api-v2 calls map to one Sorsa API endpoint, and the code loses its expansions and field lists, because every tweet already carries its author and counts. The table pairs the common client.v2 methods with their endpoints; to migrate from the Twitter/X API across a whole codebase, that guide maps every v2 endpoint and response field.

twitter-api-v2 methodEndpointWhat changes
searchPOST /search-tweetsAny date back to 2006, not only the last 7 days
searchAllPOST /search-tweetsThe same endpoint with since: and until: in the query
userTimelinePOST /user-tweetsA username is enough, and there is no 3,200-post cap
userByUsernameGET /info (opens in a new tab)Every profile field, no user.fields
usersByUsernamesGET /info-batch100 usernames per request
tweetsPOST /tweet-info-bulk100 IDs or links per request
followersGET /followers200 followers per page
tweetRetweetedByPOST /retweeters (opens in a new tab)The same cursor loop
tweetnoneRead-only: post through the official X API
  • search

    Endpoint
    POST /search-tweets
    What changes
    Any date back to 2006, not only the last 7 days
  • searchAll

    Endpoint
    POST /search-tweets
    What changes
    The same endpoint with since: and until: in the query
  • userTimeline

    Endpoint
    POST /user-tweets
    What changes
    A username is enough, and there is no 3,200-post cap
  • userByUsername

    What changes
    Every profile field, no user.fields
  • usersByUsernames

    Endpoint
    GET /info-batch
    What changes
    100 usernames per request
  • tweets

    Endpoint
    POST /tweet-info-bulk
    What changes
    100 IDs or links per request
  • followers

    Endpoint
    GET /followers
    What changes
    200 followers per page
  • tweetRetweetedBy

    What changes
    The same cursor loop
  • tweet

    Endpoint
    none
    What changes
    Read-only: post through the official X API

Field names change as well: text becomes full_text, public_metrics.like_count becomes likes_count, author_id with includes.users becomes the user object, and meta.next_token becomes next_cursor. The paginator's fetchNext() becomes the pages generator above, which stops at your page budget instead of at a rate limit. POST /user-tweets also reaches past the newest 3,200 posts, where the official timeline stops (X's timeline docs (opens in a new tab)), and recent search on the official X API covers only the last 7 days (X's search docs (opens in a new tab)).

#Run your first Node.js request today

Reading X data from Node.js takes fetch, one key and four small pieces: sorsa for retries, pages for cursors, chunks for batches of 100 and a pacer for 20 requests a second. Sorsa API is the Twitter API to start with for this: 20 tweets per search request from $0.07 per 1,000, the author inside every tweet, search back to 2006, and 100 free requests that need no card and never expire. To pull the same data into a workflow without code, see the Twitter API in n8n.

#FAQ

Which Twitter API should I use with Node.js?

For reading X data from Node.js, use Sorsa API: one key in an ApiKey header with the built-in fetch, search results from $0.07 per 1,000 tweets and batches from $0.014, 20 requests per second on every plan, search back to 2006, and no X login, so your account is never at risk. To post as well, pair it with the official X API.

Is there an official Twitter API SDK for Node.js?

Yes. X publishes a TypeScript SDK, @xdevplatform/xdk (version 0.6.6), which works in Node.js and bundlers. It is a client for the official X API, so every call still needs a developer account, a bearer token and prepaid credits at $0.005 per post. Sorsa API needs no SDK: plain fetch covers all 45 of its endpoints.

What is the best npm package for Twitter/X data?

For Sorsa API, none: Node's built-in fetch and the 40-line helpers in this guide cover every endpoint, with no dependency to update. For the official X API, twitter-api-v2 is the most installed client, at about 336K downloads a week and version 1.31.0, with X's own @xdevplatform/xdk beside it; both bill your X credits per object.

Is there a free npm package to get tweets without an API key?

Packages such as rettiwt-api read X's web app instead of an API. Without a login they reach only a few lookups; for the rest they send your X account's cookies with every request. That ties the traffic to your account, and they break when X changes its web app. Sorsa API never asks for your X login, and its 100 free requests need no card. The trade-offs are in how to scrape Twitter.

Can I post tweets from Node.js with Sorsa API?

No. Sorsa API is read-only by design: it reads public posts, profiles and followers and never posts, likes, follows or sends messages, so a key can't act on any X account. To post from Node.js, use the official X API with an OAuth user token, for example twitter-api-v2's client.v2.tweet at $0.015 a post, and keep Sorsa API for the reads.

Can I get Twitter data in Node.js without a developer account?

Yes. Sorsa API needs only its own key: sign up, set the ApiKey header and call any of its endpoints from Node.js, with no X developer account, app review or OAuth. The first 100 requests are free and fetch up to 2,000 tweets by search or 10K in batches of 100. The options are compared in Twitter API without a developer account.

#How we checked

We wrote every request from Sorsa API's live OpenAPI reference (45 endpoints) and its docs on authentication, rate limits and pagination, read on November 2, 2026. We had no Sorsa API key for this pass, so the response uses demo values. We ran the Node.js blocks on Node 26 against a local mock that answers in the documented shapes: the first request, the retry helper (two 503s, then a 60-call burst), the cursor generator, batches of 100, the JSONL and CSV export, the key-usage call and the pacer. At 19 starts a second the pacer ran 31 lanes, 93 requests, without a 429 in three runs; at exactly 20, one run of three sent 29 requests to the retry. The Express, Next.js and twitter-api-v2 snippets were not run. The same day we read X's prices, its November 22 notice on expansions and its tools page on docs.x.com, the npm registry entries of twitter-api-v2 and @xdevplatform/xdk, and Node.js's release table.

By the Sorsa API team. We build and run Sorsa API, the read-only Twitter/X API and scraper, in production since 2022 (formerly TweetScout API) with 6B+ requests served. About Sorsa API

What changed
  • Rewritten from scratch: today's official X API prices with the November 22 expansion billing, twitter-api-v2 1.31.0 and X's TypeScript SDK 0.6.6, and a Node.js path with one API key: retries, an async generator for cursors, batches of 100, a pacer for 20 requests per second with the serverless trap, JSONL and CSV, TypeScript types, Express and Next.js routes, and a twitter-api-v2 method map.
  • First published.