@apicity/x

中文文档
0.27.0发布于 2026/11/06MITTypeScriptES Module近 30 天下载 4037

X (formerly Twitter) social API provider for posting content (api.x.com).

原始使用文档
文档目录14 节
  1. Installation
  2. Quick Start
  3. Setup
  4. 1. Configure your X app
  5. 2. Load credits
  6. 3. Mint an access token
  7. 4. Use the token
  8. 5. Refresh the token
  9. Real-world example: post a video
  10. API Reference
  11. media
  12. tweets
  13. users
  14. License

@apicity/x

npm dependencies TypeScript

X (formerly Twitter) social API provider for posting content (api.x.com).

Runtime dependencies:

  • zod@^4.6.6 — request schemas attached to every POST endpoint as .schema

Installation

npm install @apicity/x
# or
pnpm add @apicity/x

Quick Start

import { createX } from "@apicity/x";

const x = createX({ accessToken: process.env.X_ACCESS_TOKEN! });

Setup

X requires an OAuth 2.0 user-context access token to post or upload media. App-only Bearer tokens are read-only and rejected by the upload and tweets endpoints.

1. Configure your X app

Open console.x.com and make sure your app lives in a Pay Per Use project — the legacy Free project is deprecated and v2 endpoints reject its tokens with client-not-enrolled. Move the app from the Apps list if needed.

Then open the app and click User authentication settings → Set up:

  • Type of App: Web App, Automated App or Bot (this yields a Client Secret)
  • App permissions: Read and write
  • Callback URI: http://127.0.0.1:8765/callback
  • Website URL: any valid URL

Save and copy the OAuth 2.0 Client ID and Client Secret.

2. Load credits

Pay-per-use bills per write (~$0.015 / post). Open Billing → Credits and load the minimum (typically $5). Without credits, write endpoints return 402 Your enrolled account does not have any credits to fulfill this request — even though authentication itself succeeds.

3. Mint an access token

Save the script below as mint-x-token.mjs and run it:

X_CLIENT_ID=<your-client-id> \
X_CLIENT_SECRET=<your-client-secret> \
  node mint-x-token.mjs

It prints an authorize URL — open it, click Authorize app, and the helper captures the redirect on 127.0.0.1:8765 and prints the access token + refresh token. Access tokens last 2 hours; the refresh token (via offline.access scope) lets you mint a new one without re-authorizing.

mint-x-token.mjs — OAuth 2.0 PKCE helper
import http from "node:http";
import crypto from "node:crypto";
import { createXOAuth } from "@apicity/x";

const CLIENT_ID = process.env.X_CLIENT_ID;
const CLIENT_SECRET = process.env.X_CLIENT_SECRET;
const REDIRECT = "http://127.0.0.1:8765/callback";
const SCOPES = [
  "tweet.read",
  "tweet.write",
  "media.write",
  "users.read",
  "offline.access",
].join(" ");

if (!CLIENT_ID || !CLIENT_SECRET) {
  console.error("Set X_CLIENT_ID and X_CLIENT_SECRET");
  process.exit(1);
}

const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto
  .createHash("sha256")
  .update(verifier)
  .digest("base64url");
const state = crypto.randomBytes(16).toString("hex");

const authURL = new URL("https://x.com/i/oauth2/authorize");
authURL.searchParams.set("response_type", "code");
authURL.searchParams.set("client_id", CLIENT_ID);
authURL.searchParams.set("redirect_uri", REDIRECT);
authURL.searchParams.set("scope", SCOPES);
authURL.searchParams.set("state", state);
authURL.searchParams.set("code_challenge", challenge);
authURL.searchParams.set("code_challenge_method", "S256");

console.log("Open this URL and click \"Authorize app\":\n" + authURL.toString());

const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, REDIRECT);
  if (!url.pathname.startsWith("/callback")) {
    res.writeHead(404).end();
    return;
  }
  const code = url.searchParams.get("code");
  if (!code || url.searchParams.get("state") !== state) {
    res.writeHead(400).end("bad state");
    server.close();
    process.exit(1);
  }
  const oauth = createXOAuth({
    clientId: CLIENT_ID,
    clientSecret: CLIENT_SECRET,
  });
  const tok = await oauth.post.v2.oauth2.token({
    grant_type: "authorization_code",
    code,
    redirect_uri: REDIRECT,
    code_verifier: verifier,
  });
  console.log(JSON.stringify(tok, null, 2));
  res.writeHead(200).end("Authorized — check your terminal.");
  server.close();
});

server.listen(8765, "127.0.0.1");

4. Use the token

import { createX } from "@apicity/x";

const x = createX({ accessToken: process.env.X_ACCESS_TOKEN! });

await x.post.v2.tweets({
  text: "hello from @apicity/x",
});

5. Refresh the token

Access tokens expire after 2 hours. Use createXOAuth with the refresh token from step 3 to mint a fresh one without re-authorizing. Refresh tokens may rotate — persist refresh_token from the response when X returns one, and keep the old one when it's omitted.

import { createXOAuth } from "@apicity/x";

declare const storedRefreshToken: string; // the refresh_token from step 3

const oauth = createXOAuth({
  clientId: process.env.X_CLIENT_ID!,
  clientSecret: process.env.X_CLIENT_SECRET!,
});

const tok = await oauth.post.v2.oauth2.token({
  grant_type: "refresh_token",
  refresh_token: storedRefreshToken,
});

Real-world example: post a video

Posting a video on X is a four-call dance — initialize a chunked media upload, append the bytes, finalize to kick off transcoding, poll until the media is ready, then attach the resulting media_id to the tweet. The flow below is taken verbatim from tests/integration/x-post-video.test.ts and replays against tests/recordings/x_*/post-video_*/recording.har, so the response shapes match what X actually returns.

import { readFileSync } from "node:fs";
import { createX } from "@apicity/x";

const x = createX({ accessToken: process.env.X_ACCESS_TOKEN! });

// 1. Initialize a chunked upload — declare the media type, total
//    byte length, and category up-front. X reserves a media_id we'll
//    thread through every later call.
const bytes = readFileSync("./jump.mp4"); // 1,318,021 bytes in the recording
const init = await x.post.v2.media.upload.initialize({
  media_type: "video/mp4",
  total_bytes: bytes.length,
  media_category: "tweet_video",
});
const mediaId = init.data.id;
// → "2050123807214718976"

// 2. Append the bytes. For files >5MB slice the buffer into
//    segments and call append once per chunk with segment_index 0..n.
await x.post.v2.media.upload.append(mediaId, {
  media: new Blob([bytes], { type: "video/mp4" }),
  segment_index: 0,
});

// 3. Finalize. X queues server-side transcoding and returns
//    processing_info.state = "pending" while the worker is busy.
const fin = await x.post.v2.media.upload.finalize(mediaId);
// fin.data.processing_info → { state: "pending", check_after_secs: 1 }

// 4. Poll status until the media is ready. Honor
//    `check_after_secs` so the loop respects X's pacing hint.
let state = fin.data.processing_info?.state ?? "succeeded";
let wait = fin.data.processing_info?.check_after_secs ?? 1;
while (state === "pending" || state === "in_progress") {
  await new Promise((r) => setTimeout(r, wait * 1000));
  const status = await x.get.v2.media.upload(mediaId);
  state = status.data.processing_info?.state ?? "succeeded";
  wait = status.data.processing_info?.check_after_secs ?? 1;
}
// status.data.processing_info → { state: "succeeded", progress_percent: 100 }

// 5. Post the tweet, attaching the now-ready media id.
const tweet = await x.post.v2.tweets({
  text: "jump",
  media: { media_ids: [mediaId] },
});

console.log(tweet.data.id);
// → "2050123819986378933"
console.log(tweet.data.text);
// → "jump https://t.co/X8cTIpcy3s"
//   X auto-appends the attached media's t.co URL to the returned
//   text — the literal request body just had "jump".

Notes

  • media_category must match the asset: tweet_video, tweet_image, tweet_gif, or amplify_video for long-form. Mismatches are rejected at finalize, not initialize.
  • Uploads expire after data.expires_after_secs (24h). If you finalize but never reference the media_id in a tweet, it is garbage-collected.
  • Errors from any step throw XError with status and the parsed body attached, so try { ... } catch (e) { if (e instanceof XError) ... } gives you the upstream errors[0].message or detail directly.

API Reference

7 endpoints across 3 groups. Each method mirrors an upstream URL path.

media

GET x.v2.media.upload

GET https://api.x.com/2/media/upload{query}

Cost tier: prohibitive

Upstream docs ↗

const res = await x.get.v2.media.upload({ /* ... */ });

Source: packages/provider/x/src/x.ts

POST x.v2.media.upload.append

POST https://api.x.com/2/media/upload/{id}/append

Cost tier: prohibitive

Upstream docs ↗

const res = await x.post.v2.media.upload.append({ /* ... */ });

Source: packages/provider/x/src/x.ts

POST x.v2.media.upload.finalize

POST https://api.x.com/2/media/upload/{id}/finalize

Cost tier: prohibitive

Upstream docs ↗

const res = await x.post.v2.media.upload.finalize({ /* ... */ });

Source: packages/provider/x/src/x.ts

POST x.v2.media.upload.initialize

POST https://api.x.com/2/media/upload/initialize

Cost tier: prohibitive

Upstream docs ↗

const res = await x.post.v2.media.upload.initialize({ /* ... */ });

Source: packages/provider/x/src/x.ts

tweets

GET x.v2.tweets

GET https://api.x.com/2/tweets/{id}{query}

Cost tier: expensive

Upstream docs ↗

const res = await x.get.v2.tweets({ /* ... */ });

Source: packages/provider/x/src/x.ts

POST x.v2.tweets

POST https://api.x.com/2/tweets

Cost tier: expensive

Upstream docs ↗

const res = await x.post.v2.tweets({ /* ... */ });

Source: packages/provider/x/src/x.ts

users

GET x.v2.users.me

GET https://api.x.com/2/users/me{query}

Cost tier: expensive

Upstream docs ↗

const res = await x.get.v2.users.me({ /* ... */ });

Source: packages/provider/x/src/x.ts

Part of the apicity monorepo.

License

MIT — see LICENSE.

版本与发布记录

@apicity/x 共发布 90 个版本,首次发布于 2026/05/11;过去 12 个月发布了 90 个版本。

版本发布日期距今
0.27.0latest2026/11/06今天
0.26.02026/11/051 天前
0.25.02026/11/033 天前
0.24.12026/11/024 天前
0.24.02026/11/014 天前
0.23.12026/11/015 天前

关于 @apicity/x 的常见问题

@apicity/x 是什么?

X (formerly Twitter) social API provider for posting content (api.x.com).

如何安装 @apicity/x?

在项目目录运行 npm install @apicity/x,或使用 pnpm add / yarn add / bun add。

@apicity/x 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@apicity/x 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@apicity/x 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@apicity/x 可以商用吗?

@apicity/x 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@apicity/x 的下载量有多少?

根据 npm 官方下载统计,@apicity/x 在 2026/10/06 – 2026/11/04 这 30 天内被下载 4,037 次,日均约 135 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@apicity/x 还在维护吗?

最新版本 0.27.0 发布于 2026/11/06(今天);至今共发布 90 个版本,其中过去 12 个月发布了 90 个;首次发布于 2026/05/11。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。