跳到文档正文

消费者示例

复制完整消费者,再用你的幂等应用处理程序替换控制台输出。

运行五分钟快速开始
在聊天中打开
按产品代码校对:2026年9月2日

构建参考消费者

组合使用以下示例。先使用完整分发器,再添加一种重连客户端、持久恢复和账号管理。

分发所有服务端操作

此 TypeScript 分发器覆盖全部 15 种服务端操作。它会先验证输入,并在遇到未知操作时忽略该消息,而不是关闭 WebSocket。

让每个处理程序保持简短。在调用策略、通知或执行边界前,先保存回执和本地投影。

完整 TypeScript 分发器

typescript
type VerifiedType = 'blue' | 'business' | 'government' | 'verified' | 'none';
type TweetVerifiedLabel = {
  badge: string | null;
  description: string;
  url: string | null;
};
 
type TweetAuthor = {
  banner?: string;
  bio?: string;
  followersCount?: number;
  followingCount?: number;
  id?: string;
  joinedAt?: number;
  location?: string;
  metrics?: {
    likes?: number;
    tweets?: number;
  };
  // Includes a leading @ when present, for example "@elonmusk".
  handle?: string;
  name?: string;
  platform?: 'twitter' | 'truth_social' | 'binance_square';
  profileImage?: string;
  url?: string;
  verifiedLabel?: TweetVerifiedLabel;
  verifiedType?: VerifiedType;
};
 
type Media = {
  url: string;
} & (
  | {
      type: 'video';
      // Every video is a progressive MP4. A public still poster is included when available.
      thumbnail?: string;
    }
  | {
      type?: 'image' | 'gif';
      thumbnail?: string;
    }
);
 
type TweetUrl = {
  url: string;
  name?: string;
  tco?: string;
};
 
type TweetMention = {
  handle?: string;
  id?: string;
  name?: string;
};
 
type TweetArticle = {
  description?: string;
  id?: string;
  publishedAt?: number;
  text?: string;
  thumbnail?: string;
  title: string;
  updatedAt?: number;
  url: string;
};
 
type TweetPollChoice = {
  id?: string;
  image?: string;
  label: string;
  votes?: number;
};
 
type TweetPoll = {
  choices: TweetPollChoice[];
  endsAt?: number;
  totalVotes?: number;
  updatedAt?: number;
};
 
type TweetContentKind = 'post' | 'reply' | 'quote' | 'retweet';
 
type TweetReference = {
  article?: TweetArticle;
  type: 'reply' | 'quote' | 'retweet';
  tweetId: string;
  text?: string;
  translatedText?: string;
  author?: TweetAuthor;
  media?: Media[];
  poll?: TweetPoll;
  quoted?: TweetReference;
  subtweet?: TweetReference;
};
 
type TweetContent = {
  tweetId: string;
  kind: TweetContentKind;
  // Original tweet text when the event includes both original and translated text.
  text: string;
  // Translation, present only when available.
  translatedText?: string;
  createdAt: number;
  author: TweetAuthor;
  article?: TweetArticle;
  link?: string;
  media?: Media[];
  mentions?: TweetMention[];
  poll?: TweetPoll;
  receivedAt?: number;
  urls?: TweetUrl[];
  ref?: TweetReference;
};
 
type TweetMeta = {
  tweetId: string;
  // Pair with tweetId when merging enrichment. Omitted means twitter on legacy frames.
  platform?: 'twitter' | 'truth_social' | 'binance_square';
  ocr?: {
    text: string;
  };
  detected?: {
    tokens?: Array<{
      symbol?: string;
      name?: string;
      contract?: string;
      chain?: string;
      networkId?: number;
      priceUsd?: number;
      sources: Array<'text' | 'ocr'>;
    }>;
    cex?: Array<{
      exchange: 'bybit' | 'binance' | 'hyperliquid';
      symbol?: string;
      priceUsd?: number;
      url?: string;
      baseAsset?: string;
      quoteAsset?: string;
      sources: Array<'text' | 'ocr'>;
    }>;
    prediction?: Array<{
      exchange: 'polymarket' | 'kalshi';
      marketId?: string;
      title?: string;
      priceUsd?: number;
      url?: string;
      sources: Array<'text' | 'ocr'>;
    }>;
  };
};
 
type TweetUpdate = {
  tweetId: string;
  // Pair with tweetId. On legacy frames, fall back to author.platform, then twitter.
  platform?: 'twitter' | 'truth_social' | 'binance_square';
  article?: TweetArticle;
  kind?: TweetContentKind;
  translatedText?: string;
  author?: TweetAuthor;
  media?: Media[];
  mentions?: TweetMention[];
  poll?: TweetPoll;
  receivedAt?: number;
  urls?: TweetUrl[];
  ref?: TweetReference;
} & (
  | {
      text?: string;
      textUpdateType?: never;
    }
  | {
      text: string;
      // Completes an earlier truncated rendering. This is not an edit signal.
      textUpdateType: 'completion';
    }
);
 
type TweetDeleteEvent = {
  tweetId: string;
  // On legacy frames, fall back to author.platform, then twitter.
  platform?: 'twitter' | 'truth_social' | 'binance_square';
  eventId?: string;
  deletedAt?: number;
  receivedAt?: number;
  author?: TweetAuthor;
  text?: string;
};
 
type TweetPinEvent = {
  tweetId: string;
  // On legacy frames, fall back to tweet.author.platform or author.platform, then twitter.
  platform?: 'twitter' | 'truth_social' | 'binance_square';
  eventId: string;
  observedAt: number;
  receivedAt?: number;
  action: 'pin';
  author: TweetAuthor;
  text?: string;
  tweet?: TweetContent;
};
 
type TweetUnpinEvent = {
  tweetId: string;
  // On legacy frames, fall back to tweet.author.platform or author.platform, then twitter.
  platform?: 'twitter' | 'truth_social' | 'binance_square';
  eventId: string;
  observedAt: number;
  receivedAt?: number;
  action: 'unpin';
  author: TweetAuthor;
  text?: string;
  tweet?: TweetContent;
};
 
type AccountEventActor = TweetAuthor & {
  websiteUrl?: string;
};
 
type ProfileUpdateEvent = {
  kind: 'PROFILE';
  eventId: string;
  observedAt: number;
  receivedAt?: number;
  actor: AccountEventActor;
  changes: {
    avatar?: string;
    banner?: string;
    bio?: string;
    handle?: string;
    location?: string;
    name?: string;
    verifiedLabel?: TweetVerifiedLabel | null;
    websiteUrl?: string | null;
  };
  previous?: {
    avatar?: string;
    banner?: string;
    bio?: string;
    handle?: string;
    location?: string;
    name?: string;
    verifiedLabel?: TweetVerifiedLabel | null;
    websiteUrl?: string | null;
  };
};
 
type FollowEvent = {
  kind: 'FOLLOW';
  eventId: string;
  observedAt: number;
  receivedAt?: number;
  actor: AccountEventActor;
  target: AccountEventActor & {
    handle: string;
  };
};
 
type UnfollowEvent = {
  kind: 'UNFOLLOW';
  eventId: string;
  observedAt: number;
  receivedAt?: number;
  actor: AccountEventActor;
  target: AccountEventActor & {
    handle: string;
  };
};
 
type AffiliateAccountIdentity = AccountEventActor & {
  id: string;
};
 
type AffiliateUpdateEvent = {
  action: 'added' | 'removed';
  eventId: string;
  observedAt: number;
  receivedAt?: number;
  organization: AffiliateAccountIdentity;
  member: AffiliateAccountIdentity;
};
 
type CalloutEvent = {
  platform: 'pump_fun';
  caller: {
    username: string;
  };
  token: {
    address: string;
    symbol?: string;
    name?: string;
    // Token artwork when available. This is not an account profile image.
    image?: string;
  };
  url?: string;
  marketCapUsd?: number;
  // Exact decimal strings preserve small onchain values.
  calloutPrice?: string;
  multiple?: number;
  maxPriceSol?: string;
  // The pump.fun Callout UUID.
  calloutId: string;
  // When pump.fun created the Callout.
  createdAt: number;
  // When TweetStream received it.
  receivedAt?: number;
};
 
type CharityAddedEvent = {
  id: string;
  name: string;
  slug: string;
  enabled: boolean;
  taxId?: string;
  location?: {
    city?: string;
    state?: string;
    country?: string;
  };
  observedAt: number;
};
 
type TwitterHandlesResult = {
  action: 'follow' | 'unfollow';
  requestId: string | null;
  results: Array<{
    input: string;
    state:
      | 'added'
      | 'already_following'
      | 'invalid_input'
      | 'duplicate'
      | 'not_found'
      | 'failed'
      | 'removed'
      | 'not_following';
    message?: string;
  }>;
  error: string | null;
};
 
type NewsSource = {
  id: string;
  name: string;
};
 
type NewsMedia = {
  url: string;
  type?: 'image' | 'video';
  caption?: string;
};
 
type NewsArticle = {
  source: NewsSource;
  url: string;
  title: string;
  publishedAt: string;
  receivedAt: number;
  modifiedAt?: string;
  primaryCategory?: string;
  categories: string[];
  author?: string;
  keywords: string[];
  description?: string;
  summary?: string;
  media: NewsMedia[];
  content?: string;
  language?: string;
  copyright?: string;
};
 
type EnvelopeBase<
  TFamily extends 'tweet' | 'account' | 'charity' | 'control' | 'news',
  TOp extends string,
  TPayload extends object,
> = {
  v: 1;
  t: TFamily;
  op: TOp;
  id?: string;
  ts: number;
  d: TPayload;
};
 
type TweetContentEnvelope = EnvelopeBase<'tweet', 'content', TweetContent>;
type TweetMetaEnvelope = EnvelopeBase<'tweet', 'meta', TweetMeta>;
type TweetUpdateEnvelope = EnvelopeBase<'tweet', 'update', TweetUpdate>;
type TweetDeleteEnvelope = EnvelopeBase<'tweet', 'delete', TweetDeleteEvent>;
type TweetPinEnvelope = EnvelopeBase<'tweet', 'pin', TweetPinEvent>;
type TweetUnpinEnvelope = EnvelopeBase<'tweet', 'unpin', TweetUnpinEvent>;
type AccountProfileEnvelope = EnvelopeBase<'account', 'profile_update', ProfileUpdateEvent>;
type AccountFollowEnvelope = EnvelopeBase<'account', 'follow', FollowEvent>;
type AccountUnfollowEnvelope = EnvelopeBase<'account', 'unfollow', UnfollowEvent>;
type AccountAffiliateEnvelope = EnvelopeBase<
  'account',
  'affiliate_update',
  AffiliateUpdateEvent
>;
type AccountCalloutEnvelope = EnvelopeBase<'account', 'callout', CalloutEvent> & {
  id: string;
};
type AccountEnvelope =
  | AccountProfileEnvelope
  | AccountFollowEnvelope
  | AccountUnfollowEnvelope
  | AccountAffiliateEnvelope
  | AccountCalloutEnvelope;
type CharityAddedEnvelope = EnvelopeBase<'charity', 'added', CharityAddedEvent> & {
  id: string;
};
type EmptyPayload = Record<string, never>;
type AuthPingCommand = { op: 'auth_ping' };
type AuthPongEnvelope = EnvelopeBase<'control', 'auth_pong', EmptyPayload>;
type TwitterHandlesResultEnvelope = EnvelopeBase<
  'control',
  'twitter_handles_result',
  TwitterHandlesResult
>;
type ControlEnvelope = AuthPongEnvelope | TwitterHandlesResultEnvelope;
type NewsEnvelope = EnvelopeBase<'news', 'article', NewsArticle> & {
  id: string;
};
 
type TweetStreamEnvelope =
  | TweetContentEnvelope
  | TweetMetaEnvelope
  | TweetUpdateEnvelope
  | TweetDeleteEnvelope
  | TweetPinEnvelope
  | TweetUnpinEnvelope
  | AccountEnvelope
  | CharityAddedEnvelope
  | NewsEnvelope
  | ControlEnvelope;
 
// Feed this router only values returned by a complete runtime decoder.
// See the reference consumer linked below for the executable decoder.
type DecodedEnvelope =
  | { kind: 'known'; event: TweetStreamEnvelope }
  | { kind: 'unknown' };
 
type ProtocolHandlers = {
  tweetContent: (event: TweetContentEnvelope) => void;
  tweetMeta: (event: TweetMetaEnvelope) => void;
  tweetUpdate: (event: TweetUpdateEnvelope) => void;
  tweetDelete: (event: TweetDeleteEnvelope) => void;
  tweetPin: (event: TweetPinEnvelope) => void;
  tweetUnpin: (event: TweetUnpinEnvelope) => void;
  accountProfileUpdate: (event: AccountProfileEnvelope) => void;
  accountFollow: (event: AccountFollowEnvelope) => void;
  accountUnfollow: (event: AccountUnfollowEnvelope) => void;
  accountAffiliateUpdate: (event: AccountAffiliateEnvelope) => void;
  accountCallout: (event: AccountCalloutEnvelope) => void;
  charityAdded: (event: CharityAddedEnvelope) => void;
  newsArticle: (event: NewsEnvelope) => void;
  authPong: (event: AuthPongEnvelope) => void;
  twitterHandlesResult: (event: TwitterHandlesResultEnvelope) => void;
  // Count and discard unknown input without logging its raw payload.
  unknown: () => void;
};
 
function assertNever(value: never): never {
  throw new Error('Unhandled validated envelope');
}
 
function route(decoded: DecodedEnvelope, handlers: ProtocolHandlers): void {
  if (decoded.kind === 'unknown') {
    handlers.unknown();
    return;
  }
 
  const event = decoded.event;
  switch (event.t) {
    case 'tweet':
      switch (event.op) {
        case 'content': handlers.tweetContent(event); return;
        case 'meta': handlers.tweetMeta(event); return;
        case 'update': handlers.tweetUpdate(event); return;
        case 'delete': handlers.tweetDelete(event); return;
        case 'pin': handlers.tweetPin(event); return;
        case 'unpin': handlers.tweetUnpin(event); return;
        default: return assertNever(event);
      }
    case 'account':
      switch (event.op) {
        case 'profile_update': handlers.accountProfileUpdate(event); return;
        case 'follow': handlers.accountFollow(event); return;
        case 'unfollow': handlers.accountUnfollow(event); return;
        case 'affiliate_update': handlers.accountAffiliateUpdate(event); return;
        case 'callout': handlers.accountCallout(event); return;
        default: return assertNever(event);
      }
    case 'charity':
      handlers.charityAdded(event);
      return;
    case 'news':
      handlers.newsArticle(event);
      return;
    case 'control':
      switch (event.op) {
        case 'auth_pong': handlers.authPong(event); return;
        case 'twitter_handles_result': handlers.twitterHandlesResult(event); return;
        default: return assertNever(event);
      }
    default:
      return assertNever(event);
  }
}

运行 5 分钟连通性测试

添加一个监控账号,复制示例并运行。看到 TweetStream connected 表示认证成功。该账号发布内容后会收到事件。

运行 TypeScript 连通性测试

typescript
// bun add ws
// TWEETSTREAM_API_KEY=ts_... bun run smoke.ts
import WebSocket from "ws";
 
const apiKey = process.env.TWEETSTREAM_API_KEY;
if (!apiKey) {
  throw new Error("Missing TWEETSTREAM_API_KEY");
}
 
const ws = new WebSocket("wss://ws-global.tweetstream.io/ws", [
  "tweetstream.v1",
  `tweetstream.auth.token.${apiKey}`,
]);
 
ws.on("open", () => {
  console.log("TweetStream connected");
});
 
ws.on("message", (raw) => {
  const event = JSON.parse(raw.toString());
  console.log(event.t, event.op, event.d);
});
 
ws.on("close", (code, reason) => {
  console.log("TweetStream closed", code, reason.toString());
});
 
ws.on("unexpected-response", (_request, response) => {
  console.error("Connection rejected", response.statusCode, response.statusMessage);
  response.resume();
  process.exitCode = 1;
});
 
ws.on("error", (error) => {
  console.error("WebSocket error", error.message);
});

连接

美国境内使用 wss://ws-iad.tweetstream.io/ws,其他地区使用 wss://ws-global.tweetstream.io/ws。服务端返回 tweetstream.v1,不会回显携带认证 token 的协议。示例通过指数退避重连。短连接会继续增加等待时间,稳定连接 30 秒后重置。

使用 Node.js 连接

typescript
import WebSocket from "ws";
 
type StreamEvent = {
  t?: string;
  op?: string;
  d?: {
    author?: { handle?: string };
    detected?: unknown;
    text?: string;
  };
};
 
const apiKey = process.env.TWEETSTREAM_API_KEY;
if (!apiKey) {
  throw new Error("Missing TWEETSTREAM_API_KEY");
}
 
let retry = 0;
let healthyTimer: ReturnType<typeof setTimeout> | undefined;
let reconnectTimer: ReturnType<typeof setTimeout> | undefined;
 
function scheduleReconnect(reason: string) {
  if (reconnectTimer) return;
 
  const delayMs = Math.min(30_000, 1_000 * 2 ** retry) + Math.floor(Math.random() * 500);
  retry += 1;
  console.warn(`Reconnecting in ${delayMs}ms: ${reason}`);
  reconnectTimer = setTimeout(() => {
    reconnectTimer = undefined;
    connect();
  }, delayMs);
}
 
function connect() {
  const ws = new WebSocket("wss://ws-global.tweetstream.io/ws", [
    "tweetstream.v1",
    `tweetstream.auth.token.${apiKey}`,
  ]);
 
  ws.on("open", () => {
    if (reconnectTimer) clearTimeout(reconnectTimer);
    reconnectTimer = undefined;
    healthyTimer = setTimeout(() => {
      retry = 0;
      healthyTimer = undefined;
    }, 30_000);
    console.log("TweetStream connected");
  });
 
  ws.on("message", (raw) => {
    const event = JSON.parse(raw.toString()) as StreamEvent;
 
    if (event.t === "tweet" && event.op === "content") {
      const tweet = event.d;
      console.log(tweet?.author?.handle, tweet?.text);
    }
 
    if (event.t === "tweet" && event.op === "meta") {
      console.log("enrichment", event.d?.detected);
    }
  });
 
  ws.on("close", (code, reason) => {
    if (healthyTimer) clearTimeout(healthyTimer);
    healthyTimer = undefined;
    scheduleReconnect(`close ${code}: ${reason.toString()}`);
  });
 
  ws.on("unexpected-response", (_request, response) => {
    console.error("Connection rejected", response.statusCode, response.statusMessage);
    response.resume();
    if (response.statusCode === 429 || response.statusCode === 503) {
      scheduleReconnect(`HTTP ${response.statusCode}`);
      return;
    }
    process.exitCode = 1;
  });
 
  ws.on("error", (error) => {
    console.error("WebSocket error", error.message);
  });
}
 
connect();

使用 Python 连接

使用任何能发送这两个子协议的 WebSocket 运行时。正常关闭和传输错误都会通过指数退避重连。短连接会继续增加等待时间,稳定连接 30 秒后重置。

Python 重连客户端

python
import asyncio
import json
import os
import websockets
 
API_KEY = os.environ["TWEETSTREAM_API_KEY"]
URI = "wss://ws-global.tweetstream.io/ws"
PROTOCOLS = ["tweetstream.v1", f"tweetstream.auth.token.{API_KEY}"]
 
async def main():
    loop = asyncio.get_running_loop()
    retry = 0
    while True:
        connected_at = None
        reason = "connection closed"
        try:
            async with websockets.connect(URI, subprotocols=PROTOCOLS) as ws:
                connected_at = loop.time()
                async for raw in ws:
                    event = json.loads(raw)
                    if event["t"] == "tweet" and event["op"] == "content":
                        tweet = event["d"]
                        print(tweet.get("author", {}).get("handle"), tweet.get("text"))
        except Exception as error:
            reason = str(error)
 
        if connected_at is not None and loop.time() - connected_at >= 30:
            retry = 0
        wait = min(30, 2 ** retry)
        retry = min(retry + 1, 5)
        print(f"reconnecting in {wait}s after {reason}")
        await asyncio.sleep(wait)
 
asyncio.run(main())

重连后回补

固定 startDateendDate,在其他筛选条件不变的情况下持续使用 nextCursor,直到返回 null。以幂等方式处理每行。如果中断后的回放需要继续而不是重启,请一起保存时间范围和 cursor。

History 返回已存事件,不保证完整性或保留期限。AFFILIATE 返回已存的关联账号变更,不是当前列表快照。delete、pin 和 unpin 仍只在实时流中发送。

回放恢复窗口

typescript
type HistoryRow = {
  time: string;
  tweetId: string;
};
 
type HistoryPage = {
  data: Array<HistoryRow>;
  metadata: { nextCursor: string | null };
};
 
async function replayAfterReconnect(
  checkpoint: string,
  processEvent: (event: HistoryRow) => Promise<void>,
  saveCheckpoint: (eventTime: string) => Promise<void>,
) {
  const apiKey = process.env.TWEETSTREAM_API_KEY;
  if (!apiKey) throw new Error("Missing TWEETSTREAM_API_KEY");
 
  const url = new URL("https://api.tweetstream.io/api/history");
  const replayEnd = new Date().toISOString();
  url.searchParams.set("handles", "marketdesk");
  url.searchParams.set("startDate", checkpoint);
  url.searchParams.set("endDate", replayEnd);
  url.searchParams.set("limit", "1000");
  url.searchParams.set("type", "TWEET");
 
  const pages: Array<Array<HistoryRow>> = [];
  let cursor: string | null = null;
  do {
    if (cursor) url.searchParams.set("cursor", cursor);
    const response = await fetch(url, {
      headers: { Authorization: `Bearer ${apiKey}` },
    });
    if (!response.ok) {
      throw new Error(`Replay failed (${response.status}): ${await response.text()}`);
    }
 
    const page = (await response.json()) as HistoryPage;
    pages.push(page.data);
    cursor = page.metadata.nextCursor;
  } while (cursor);
 
  for (const event of pages.reverse().flatMap((page) => [...page].reverse())) {
    await processEvent(event);
  }
  await saveCheckpoint(replayEnd);
}

请求 History

重连后或查看特定时间范围时,使用 GET /api/history 回放已存事件。

检查权限

Pro 和 Scale 可以请求所有 History 事件类型。有效或试用中的 Ultra 可使用拥有该账号的标准 key 或 Ultra key 请求 AFFILIATE。支持的类型为 TWEETPROFILEFOLLOWAFFILIATE。delete、pin 和 unpin 仍只在实时流中发送。

设置筛选条件

不提供账号筛选时,History 会查询该 key 当前监控的所有有效账号。筛选条件只能包含该 key 当前监控的账号。

参数必填说明
handle, handles, handle[], handles[]单个账号、重复参数或逗号分隔的多个账号
startDateISO datetime 下限
endDateISO datetime 上限
limit默认 100,最大 1000
typeTWEET、PROFILE、FOLLOW 或 AFFILIATE,默认为 TWEET
cursor上一页返回的不透明 nextCursor;其他筛选条件必须保持不变

History 请求

typescript
const apiKey = process.env.TWEETSTREAM_API_KEY;
if (!apiKey) throw new Error("Missing TWEETSTREAM_API_KEY");
 
const response = await fetch(
  "https://api.tweetstream.io/api/history?limit=25&type=TWEET",
  {
    headers: {
      Authorization: `Bearer ${apiKey}`,
    },
  },
);
 
if (!response.ok) {
  throw new Error(`History failed (${response.status}): ${await response.text()}`);
}
 
const page = await response.json();
console.log(page.data);

添加和移除账号

从后端调用 REST 端点来修改监控列表。

发送账号 handle

accounts 中发送一个 handle 或 handle 数组。开头的 @ 可以省略,匹配不区分大小写。

添加账号

typescript
const apiKey = process.env.TWEETSTREAM_API_KEY;
if (!apiKey) throw new Error("Missing TWEETSTREAM_API_KEY");
 
const response = await fetch("https://api.tweetstream.io/api/add-account", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    accounts: ["marketdesk", "realDonaldTrump"],
  }),
});
 
if (!response.ok && response.status !== 207) {
  throw new Error(`Add failed (${response.status}): ${await response.text()}`);
}
 
console.log(await response.json());

移除账号

typescript
const apiKey = process.env.TWEETSTREAM_API_KEY;
if (!apiKey) throw new Error("Missing TWEETSTREAM_API_KEY");
 
const response = await fetch("https://api.tweetstream.io/api/remove-account", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    accounts: "marketdesk",
  }),
});
 
if (!response.ok && response.status !== 207) {
  throw new Error(`Remove failed (${response.status}): ${await response.text()}`);
}
 
console.log(await response.json());

选择凭证范围

标准 API key 更新标准监控账号列表。有效的 Ultra key 更新 Ultra 选择,并执行其已付费账号上限。

读取每条结果

响应包含每个 handle 的处理结果。重试前应读取每行状态,而不能只看 HTTP 状态。

命令结果

json
{
  "action": "follow",
  "requestId": "8b4f9c9c-9e7b-4a0c-9c7d-2d4d6f0a9a25",
  "error": null,
  "results": [
    {
      "input": "marketdesk",
      "state": "added"
    },
    {
      "input": "realDonaldTrump",
      "state": "added"
    }
  ],
  "summary": {
    "failed": 0,
    "succeeded": 2,
    "total": 2
  }
}