跳到正文

Node.js 版 Twitter API:2026 年指南,附可运行的 fetch 代码

Node.js 用内置的 fetch 就能读取 Twitter/X 数据:在请求头里带上 API 密钥,发送 HTTPS 请求,解析 JSON,再把返回的游标传回去获取下一页。官方 X API 每读取一条帖子收费 $0.005。

  • 更新于
  • 发布于
  • 阅读约 13 分钟

简短回答: Node.js 读取 X 数据不需要任何包,fetch 是内置的。官方 X API 即使通过 twitter-api-v2 调用,也需要开发者账号和预付额度:每条帖子 $0.005,从 2026 年 11 月 22 日起连作者一起读取是 $0.015。读取公开数据可以用 Sorsa API:一个 ApiKey 请求头,每次请求 20 条推文,每 1,000 条 $0.07 起;100 次免费请求可以获取 2,000 条。

接口
POST /search-tweets
每页条数
每次请求 20 条推文
价格
每 1,000 条推文 $0.07 起

#2026 年哪个 npm 包能配合官方 X API 使用?

在 Node.js 里调用官方 X API 有两个包。社区客户端 twitter-api-v2 在 2026 年 10 月 25 日发布了 1.31.0 版,每周安装量约 33.6 万次。X 自己的 TypeScript SDK @xdevplatform/xdk 目前是 0.6.6。两者封装的都是同一个付费平台,所以都离不开开发者账号、bearer 令牌和预付额度。同样的请求用 Python 来写(对应的包是 Tweepy 和 xdk),见 Python 版 Twitter API指南。

额度是旧的 Node 教程最容易略过的部分。X 对新开发者没有免费额度,响应里返回的每个对象都要计费:每条帖子 $0.005,每个用户 $0.010,每月最多读取 300 万条帖子(见 X 的价格表(在新标签页打开))。从 2026 年 11 月 22 日起,通过扩展字段带出的作者和媒体与直接读取同价(见 X 在 10 月 23 日发布的通知(在新标签页打开))。这样一来,典型的 twitter-api-v2 搜索会为附带的每个作者付费:

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

它的分页器还有第二个陷阱:该库自己的文档说明,对分页器使用 for await 循环会一直取数据,直到结果取完或触发速率限制,而且每一页都要计费。所有费率、上限和日期都整理在 X API 价格一文里。

官方路线在第一次调用前需要什么

  • 在 console.x.com 开通的开发者账号,用同意 X 开发者协议的 X 账号开通。
  • 一个应用,它的 bearer 令牌 X 只显示一次。
  • 提前购买的额度,按返回的每条帖子、每个用户和每个媒体对象扣费。
  • 发帖、点赞或关注等操作需要的 OAuth 用户令牌。

读取公开帖子和用户资料用不到这些。自带密钥的 X 数据 API 走普通 HTTPS 就能回答,Node 的 fetch 就是完整的客户端。

#用一个 API 密钥在 Node.js 中获取推文

要从 Node.js 读取 X 数据,可以用 Sorsa API,一款 Twitter(X)API 与爬虫:用一个 ApiKey 请求头代替 OAuth,所有套餐(包括免费账号)都是每秒 20 次请求,搜索结果每 1,000 条推文 $0.07 起。每条推文都带着作者的完整资料,所以代码里不用设置扩展字段,也不用列字段清单。Sorsa API 从不要求登录 X,所以你自己的 X 账号不会有任何风险。

准备密钥并发送第一个请求

  1. 获取密钥。 注册后,在控制台复制你的 API 密钥。前 100 次请求免费,无需绑卡,永不过期。

  2. 检查 Node 版本并保存密钥。 运行 node -v:2026 年 11 月的 LTS 版本线是 22、24 和 26,三者都内置了 fetch 和 --env-file。把密钥放进 git 忽略的 .env 文件,并且只在服务端代码里调用 API。

    Terminal
    node -v                                  # v22 or newer
    echo 'SORSA_API_KEY=your_key' > .env
    echo '.env' >> .gitignore
    
  3. 搜索推文。 把下面的代码保存为 first.mjs,然后运行 node --env-file=.env first.mjs。它用 X 的搜索语法向 POST /search-tweets(在新标签页打开) 发送一条查询,密钥放在 ApiKey 请求头里;.mjs 扩展名会启用顶层 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. 读取这一页。 每个响应最多包含 20 条推文,以及用于获取下一页的 next_cursor。下面的值是按文档格式给出的演示数据。

    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"
    }
    

下面每个字段都直接来自响应;JavaScript 类型是 res.json() 给你的类型,ID 刻意保持为字符串。

字段JavaScript 类型含义
idstring推文的 19 位 ID:千万不要转成 Number
full_textstring帖子的完整文本
created_atstringUTC 的 ISO 8601 格式:new Date(t.created_at) 可以直接解析
likes_count, retweet_count, reply_count, quote_countnumber互动数
view_count, bookmark_countnumber浏览数和收藏数
userobject作者的资料:username、followers_count 等 19 个字段
quoted_status, retweeted_statusobject 或不存在被引用或被转发的推文,结构相同
  • id

    JavaScript 类型
    string
    含义
    推文的 19 位 ID:千万不要转成 Number
  • full_text

    JavaScript 类型
    string
    含义
    帖子的完整文本
  • created_at

    JavaScript 类型
    string
    含义
    UTC 的 ISO 8601 格式:new Date(t.created_at) 可以直接解析
  • likes_count, retweet_count, reply_count, quote_count

    JavaScript 类型
    number
    含义
    互动数
  • view_count, bookmark_count

    JavaScript 类型
    number
    含义
    浏览数和收藏数
  • user

    JavaScript 类型
    object
    含义
    作者的资料:username、followers_count 等 19 个字段
  • quoted_status, retweeted_status

    JavaScript 类型
    object 或不存在
    含义
    被引用或被转发的推文,结构相同

#用一个辅助函数重试 429 和服务器错误

让每次调用都经过同一个函数,只重试值得重试的情况。429 表示你的密钥在一秒内发起了超过 20 次请求;5xx 或超时则是临时故障。本指南后面的代码都会导入这个 sorsa 辅助函数。

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

401 或 403 永远不该重试:说明密钥有误或缺失,或者额度已用完,message 字段会写明是哪种。失败的请求不会计费,所以重试不花钱。Sorsa API 的所有接口都只读,因此重复发送 /search-tweets 这样的 POST 请求不会重复创建任何内容。Sorsa API 不返回速率限制响应头,因为窗口每秒重置一次:等一秒就能消除 429。

#用 next_cursor 翻页读取结果

游标是通往下一页的不透明键:把 next_cursor 原样传回去,响应里没有它时就停止。异步生成器很适合这个循环,因为读取各页的代码完全看不到游标。

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);
}
你的代码 for await 产出第 1 页 产出第 2 页 return query query + "c1" query + "c2" 请求 页 第 1 页 第 2 页 第 3 页 Sorsa API POST /search-tweets 20 条推文 next_cursor: "c1" 17 条推文 next_cursor: "c2" 12 条推文 next_cursor: null
生成器把每个游标传回去,遇到 null 就停止;中间出现不满一页的结果并不代表到了末尾。

搜索和时间线每页最多返回 20 条推文,而且一页可能少于 20 条但后面仍有数据,所以不能靠条数判断是否结束。同一个游标链上要保持查询和 order 不变,并把 maxPages 当作预算:50 页就是 50 次请求,不管查询匹配了多少内容。GET 接口则把游标放在查询字符串里:GET /followers(在新标签页打开) 每页最多返回 200 份用户资料,用 query.set("next_cursor", page.next_cursor) 往后翻。POST /user-tweets(在新标签页打开) 可以用同一个生成器;如果要用可续传的游标下载某个用户的全部推文,那篇指南有完整脚本。

#每次请求获取 100 条推文或用户资料

如果你已经有一批 ID 或用户名,就分批发送。POST /tweet-info-bulk(在新标签页打开) 在 tweet_links 里最多接受 100 个推文 ID 或链接,GET /info-batch(在新标签页打开) 最多接受 100 个用户名;每一批算一次请求。

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 把 usernames 当作重复参数来读取(usernames=a&usernames=b),所以要用 append 来构造。如果把数组传给 new URLSearchParams({ usernames }),用户名会被逗号连成一个值。一次请求的价格与返回内容的多少无关,所以一批 100 条推文的价格和一条推文一样:每 1,000 条推文 $0.014 起,比官方 X API 的 $5.00 便宜 250 多倍。批量响应不带游标;整批数据都在 tweets 或 users 里。

#用一个密钥跑到每秒 20 次请求

Sorsa API 在所有套餐上对每个密钥允许每秒 20 次请求,一个 Node 进程就能轻松达到:事件循环在单个线程上就能同时挂起几十个请求。需要遵守的是请求的发起速率,所以在每次调用前放一个调度器,并把工作拆成相互独立的链。一条游标链无法并行,因为每一页都需要上一页的游标,所以每个账号跑一条链,或者搜索时每天跑一条链。

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");

这个调度器不需要加锁:JavaScript 一次只运行一个回调,所以两条链不可能拿到同一个起始时间。每秒 19 次发起,给上限留出了一次的余量,因为定时器抖动可能把第 20 次推进下一个窗口。万一还有 429 漏过去,辅助函数里的重试会处理。常见的错误是不加调度器就用 Promise.all:同时发出 60 个调用,其中 40 个会直接撞上 429,并花好几秒在退避上。

一个 Node.js 进程 pacer(19) 10 月 1 日 10 月 2 日 10 月 3 日 10 月 4 日 一个 API 密钥 每秒启动 19 次 低于每密钥 20 次的限制 三个 serverless 实例 pacer(19) pacer(19) pacer(19) 各自只计自己的启动数 同一个 API 密钥 每秒启动 57 次 其中 37 次收到 429 并等待
调度器只统计自己进程里的发起次数,所以每个密钥只配一个调度器,或者在实例之间分摊速率。

无服务器平台会悄悄打破这条规则。Lambda、Vercel 或 Cloud Run 上的每个函数实例都会加载自己的 lanes.mjs。因此三个热实例会在一个密钥上运行三个调度器,合起来每秒最多发起 57 个请求。给每个实例分一份速率(三个实例就用 pacer(6)),或者把批量任务放在一个 worker 里跑。上限是按密钥统计的,所以第二个 worker 配第二个密钥,就有自己的每秒 20 次。按每秒 20 次请求算,每批 100 条的批量请求每秒最多拉取 2,000 条推文,搜索页每秒最多 400 条。

#把推文保存为 JSONL 或 CSV

每页一到就写入,不要把所有推文都留在内存里。JSON Lines 每行一个 JSON 对象,保留所有字段,追加写入也安全;CSV 可以直接用 Excel 和 Google Sheets 打开。磁盘写不过来时,下面两个流都会等待 drain。

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();

CSV 开头的字节顺序标记告诉 Excel 这个文件是 UTF-8,所以 emoji 和带重音的字符都能正确显示。每个单元格都加了引号,所以推文里的逗号、引号和换行都会留在自己的列里。500 页最多得到 1 万条推文,只需 500 次请求;不管运行多久,内存占用都保持平稳。

须知

JavaScript 的数字只能精确表示不超过 2^53(约 9 千万亿)的整数,而推文 ID 有 19 位。Number("1985432176540981234") 返回 1985432176540981200,那是另一条推文。Sorsa API 把所有 ID 和游标都以字符串发送,所以请保持它们是字符串,按字符串比较,需要按 ID 排序时再用 BigInt(id)。

#为响应添加 TypeScript 类型

TypeScript 用户只需几个与 API 字段同名的接口,就能获得类型安全。下面这些依据线上的 OpenAPI 参考文档,涵盖大多数代码会读取的字段;其余字段见参考文档。

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;
}

可选字段带有 ?:被引用或被转发的推文只存在于引用和转发上,所以编译器会强制你先检查再读取。在边界处做类型转换,const page = (await sorsa(path, { body })) as TweetsPage,其余代码就都受到检查。Node 22.18 及更高版本通过去掉类型直接运行 .ts 文件,所以 node --env-file=.env job.ts 无需构建步骤就能运行。如果想一次覆盖所有接口,可以用 openapi-typescript 这类 OpenAPI 生成器,把完整的参考文档转成类型。

#能在浏览器的 JavaScript 里调用 Twitter API 吗?

不能:放在浏览器代码里的 API 密钥,任何打开开发者工具的人都能读到,而且 Sorsa API 是为服务端调用设计的,浏览器调用会遇到 CORS 错误。在你的服务器上加一个小路由,把密钥留在服务器上,让前端调用你的路由。

// 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);

这样浏览器就对自己的域名执行 fetch("/api/tweets?q=nodejs"),永远看不到密钥。同样的做法适用于任何服务端运行环境:Cloudflare Worker 或 Lambda 函数把密钥放在自己的环境变量里,页面只和它通信。当很多访客请求同样的内容时,可以把路由的结果缓存一分钟:Sorsa API 每次请求都返回最新数据,一份缓存的结果可以只用一次请求服务所有人。

#一个 Node.js 任务要花多少钱

Sorsa API 按请求收费,与接口返回多少内容无关:搜索结果每 1,000 条推文 $0.07 起,批量请求每 1,000 条 $0.014 起,失败的请求不计费。官方 X API 则按对象收费:每 1,000 条帖子 $5.00,从 2026 年 11 月 22 日起,如果代码扩展出每条帖子的作者(大多数 twitter-api-v2 搜索都是这样),每 1,000 条最高 $15.00。

代码通过搜索采集的推文数

官方 X API

$5,000

采集 100 万 条推文

每条帖子 $0.005

Sorsa API

$70 起

采集 100 万 条推文

每 1,000 条 $0.07 起

每条推文都自带作者资料

下面的构建器会为一个任务生成 Node.js 循环,并显示请求次数、单个密钥下最快的耗时,以及按 Sorsa API 最低费率计算的成本。

任务
数量
job.mjs
// sorsa() 和 pages() 来自上面的辅助函数
const body = { query: "nodejs lang:en" };
for await (const page of pages("/search-tweets", body, 5000)) {
  save(page.tweets); // 每页最多 20 条推文
}
请求数
5,000
单个密钥下最快
约 4 分钟
Sorsa API
低至 $7

按每秒 20 次请求计时,按每 1,000 次请求 $1.40 计费。一次搜索就是一条游标链,请拆成多个日期窗口,让各条通道并行运行。

耗时是单个密钥允许的最快速度;单条游标链只能按往返耗时的节奏推进,所以大任务请拆成多条通道。

每 1,000 次请求的费率从 Starter 的 $4.90 到 Hyperscale 的 $1.40 不等,所有接口使用同一个费率。查看剩余额度不花钱:GET /key-usage-info(在新标签页打开) 永远不会计入你的请求次数。

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

#把 twitter-api-v2 代码迁移到 Sorsa API

twitter-api-v2 的大多数调用都对应 Sorsa API 的一个接口,而且代码不再需要扩展字段和字段清单,因为每条推文已经带有作者和各项计数。下表把常用的 client.v2 方法和对应接口配成了对;如果要在整个代码库里从 Twitter/X API 迁移,那篇指南列出了每个 v2 接口和响应字段的对应关系。

twitter-api-v2 方法接口有什么变化
searchPOST /search-tweets可查 2006 年以来的任意日期,而不只是最近 7 天
searchAllPOST /search-tweets同一个接口,在查询里加上 since: 和 until:
userTimelinePOST /user-tweets只需用户名,也没有 3,200 条帖子的上限
userByUsernameGET /info(在新标签页打开)返回全部资料字段,无需 user.fields
usersByUsernamesGET /info-batch每次请求 100 个用户名
tweetsPOST /tweet-info-bulk每次请求 100 个 ID 或链接
followersGET /followers每页 200 个粉丝
tweetRetweetedByPOST /retweeters(在新标签页打开)同样的游标循环
tweet无只读:发帖请走官方 X API
  • search

    接口
    POST /search-tweets
    有什么变化
    可查 2006 年以来的任意日期,而不只是最近 7 天
  • searchAll

    接口
    POST /search-tweets
    有什么变化
    同一个接口,在查询里加上 since: 和 until:
  • userTimeline

    接口
    POST /user-tweets
    有什么变化
    只需用户名,也没有 3,200 条帖子的上限
  • userByUsername

    有什么变化
    返回全部资料字段,无需 user.fields
  • usersByUsernames

    接口
    GET /info-batch
    有什么变化
    每次请求 100 个用户名
  • tweets

    接口
    POST /tweet-info-bulk
    有什么变化
    每次请求 100 个 ID 或链接
  • followers

    接口
    GET /followers
    有什么变化
    每页 200 个粉丝
  • tweetRetweetedBy

    有什么变化
    同样的游标循环
  • tweet

    接口
    无
    有什么变化
    只读:发帖请走官方 X API

字段名也会变:text 变成 full_text,public_metrics.like_count 变成 likes_count,author_id 加 includes.users 变成 user 对象,meta.next_token 变成 next_cursor。分页器的 fetchNext() 变成上面的 pages 生成器,它在达到你设定的页数预算时停止,而不是撞上速率限制才停。POST /user-tweets 还能越过最新 3,200 条帖子的界限,而官方时间线到这里就停了(见 X 的时间线文档(在新标签页打开));官方 X API 的近期搜索只覆盖最近 7 天(见 X 的搜索文档(在新标签页打开))。

#今天就发出你的第一个 Node.js 请求

用 Node.js 读取 X 数据只需要 fetch、一个密钥和四个小部件:负责重试的 sorsa、负责游标的 pages、负责每批 100 条的 chunks,以及限制在每秒 20 次请求的调度器。要做这件事,Sorsa API 是值得先用的 Twitter API:每次搜索请求 20 条推文,每 1,000 条 $0.07 起,作者信息就在每条推文里,搜索可回溯到 2006 年,另有 100 次无需绑卡、永不过期的免费请求。如果想不写代码就把同样的数据拉进工作流,见 n8n 版 Twitter API。

#常见问题

Node.js 应该用哪个 Twitter API?

用 Node.js 读取 X 数据,建议用 Sorsa API:在内置 fetch 的 ApiKey 请求头里放一个密钥,搜索结果每 1,000 条推文 $0.07 起,批量请求每 1,000 条 $0.014 起,所有套餐都是每秒 20 次请求,搜索可回溯到 2006 年,而且无需登录 X,你的账号不会有风险。如果还要发帖,可以搭配官方 X API。

Node.js 有官方的 Twitter API SDK 吗?

有。X 发布了 TypeScript SDK @xdevplatform/xdk(0.6.6 版),可以在 Node.js 和打包工具中使用。它是官方 X API 的客户端,所以每次调用仍然需要开发者账号、bearer 令牌,以及每条帖子 $0.005 的预付额度。Sorsa API 不需要 SDK:普通的 fetch 就能调用它的全部 45 个接口。

获取 Twitter/X 数据最好用哪个 npm 包?

对 Sorsa API 来说,不需要任何包:Node 内置的 fetch 加上本指南里约 40 行的辅助函数就能覆盖所有接口,没有需要更新的依赖。对官方 X API 来说,twitter-api-v2 是安装量最大的客户端,每周下载约 33.6 万次,版本为 1.31.0,旁边还有 X 自己的 @xdevplatform/xdk;两者都按对象从你的 X 额度里扣费。

有不需要 API 密钥、就能获取推文的免费 npm 包吗?

rettiwt-api 之类的包读取的是 X 的网页应用,而不是 API。不登录时,它们只能做少数几种查询;其余调用每次请求都会带上你 X 账号的 cookie。这会把流量和你的账号绑在一起,X 一改网页应用它们就会失效。Sorsa API 从不要求你的 X 登录信息,它的 100 次免费请求也无需绑卡。各种方案的取舍见如何抓取 Twitter。

能用 Sorsa API 在 Node.js 里发推文吗?

不能。Sorsa API 从产品设计上就是只读:它读取公开的帖子、用户资料和粉丝,从不发帖、点赞、关注或发送消息,所以一个密钥无法对任何 X 账号执行操作。要在 Node.js 里发帖,请使用带 OAuth 用户令牌的官方 X API,例如 twitter-api-v2 的 client.v2.tweet,每条帖子 $0.015,而读取仍交给 Sorsa API。

没有开发者账号,能在 Node.js 里获取 Twitter 数据吗?

可以。Sorsa API 只需要它自己的密钥:注册,设置 ApiKey 请求头,就能在 Node.js 里调用它的任何接口,不需要 X 开发者账号、应用审核或 OAuth。前 100 次请求免费,通过搜索最多能获取 2,000 条推文,用每批 100 条的批量请求则最多能获取 1 万条。各种方案的比较见没有开发者账号如何使用 Twitter API。

#我们如何核实

我们根据 Sorsa API 线上的 OpenAPI 参考文档(45 个接口)以及它关于认证、速率限制和分页的文档编写了每个请求,阅读日期为 2026 年 11 月 2 日。这一轮我们没有 Sorsa API 密钥,所以响应使用的是演示值。我们在 Node 26 上,对着一个按文档格式应答的本地模拟服务运行了这些 Node.js 代码块:第一个请求、重试辅助函数(两次 503,然后一轮 60 个调用的突发)、游标生成器、每批 100 条的批量请求、JSONL 和 CSV 导出、密钥用量调用,以及调度器。每秒 19 次发起时,调度器跑了 31 条链、93 个请求,三轮都没有出现 429;恰好每秒 20 次时,三轮中有一轮向重试逻辑送去了 29 个请求。Express、Next.js 和 twitter-api-v2 的代码片段没有运行。同一天,我们还读了 docs.x.com 上 X 的价格、11 月 22 日关于扩展字段的通知和它的工具页面,twitter-api-v2 与 @xdevplatform/xdk 在 npm 注册表中的条目,以及 Node.js 的发布时间表。

Sorsa API 团队撰写。 我们开发和运营 Sorsa API,这是只读的 Twitter/X API 与爬虫,2022 年起运行于生产环境(前身为 TweetScout API),累计处理请求超 60 亿次。 关于 Sorsa API

更新内容
  • 从头重写:更新为官方 X API 当前价格(含 11 月 22 日起生效的扩展字段计费)、twitter-api-v2 1.31.0 和 X 官方 TypeScript SDK 0.6.6,并给出只用一个 API 密钥的 Node.js 方案:重试、用于游标的异步生成器、每批 100 条的批量请求、限制在每秒 20 次请求的调度器(含无服务器环境的陷阱)、JSONL 和 CSV、TypeScript 类型、Express 和 Next.js 路由,以及 twitter-api-v2 方法对照表。
  • 首次发布。