History API
通过 History API 回放已存内容、资料、关注和关联账号事件。
请求历史记录
- GET
/api/history
使用 GET /api/history 在重连后回放已存事件,或检查指定时间窗口。
检查权限
Pro、Scale 和 Ultra Speed 可以请求所有 History 事件类型。标准密钥查询基础套餐的监控账号;Ultra 密钥查询指定的 Ultra 账号。支持的类型为 TWEET、PROFILE、FOLLOW 和 AFFILIATE。delete、pin 和 unpin 仍只在实时流中发送。
设置筛选条件
不提供账号筛选时,History 会查询该密钥当前监控的所有有效账号。筛选条件只能包含该密钥当前监控的账号。
| 参数 | 必填 | 说明 |
|---|---|---|
handle, handles, handle[], handles[] | 否 | 单个账号、重复参数或逗号分隔的多个账号 |
startDate | 否 | ISO datetime 下限 |
endDate | 否 | ISO datetime 上限 |
limit | 否 | 默认 100,最大 1000 |
type | 否 | TWEET、PROFILE、FOLLOW 或 AFFILIATE,不区分大小写,默认为 TWEET |
cursor | 否 | 上一页返回的不透明 nextCursor;其他筛选条件必须保持不变 |
History 请求
typescriptconst 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) {
const detail = await response.text();
throw new Error(`History failed (${response.status}): ${detail}`);
}
const page = await response.json();
console.log(page.data);在有界时间范围内翻页
History 每次请求最多返回 1000 行,并按稳定的新到旧顺序排列。
固定起止时间
记录一个 endDate,选择 startDate,并在此次回放的每一页中保持两者不变。
使用 nextCursor 继续
将返回的每个 nextCursor 与相同的 type、账号和日期范围一起发送。cursor 为 null 时,此时间范围已结束。以幂等方式处理每行,并在恢复期间继续处理实时生命周期事件,因为 History 会分别回放已存事件类别,不会回放仅实时发送的状态变更。
读取响应
先读取数据行和请求元数据,再推进回放窗口。
读取数据行和元数据
结果按稳定的新到旧顺序遍历。metadata.count 是本页行数,metadata.nextCursor 用于继续相同的筛选查询,cursor 为 null 时结果集结束。较早保存的 FOLLOW 行可能带有空 target;这表示目标身份不可用,仍应继续处理该事件。
History 响应类型
显示完整定义(242 行)
History 响应类型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;
// History stores X and Truth Social posts; Binance Square content is live-only.
platform?: 'twitter' | 'truth_social';
profileImage?: string;
// The account's own website, when it has one.
url?: string;
verifiedLabel?: TweetVerifiedLabel;
verifiedType?: VerifiedType;
};
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;
};
type UnfollowEvent = {
kind: 'UNFOLLOW';
eventId: string;
observedAt: number;
receivedAt?: number;
actor: AccountEventActor;
target: AccountEventActor;
};
type AffiliateAccountIdentity = AccountEventActor & {
id: string;
};
type AffiliateUpdateEvent = {
action: 'added' | 'removed';
eventId: string;
observedAt: number;
receivedAt?: number;
organization: AffiliateAccountIdentity;
member: AffiliateAccountIdentity;
};
type TweetMeta = {
tweetId: string;
// Pair with tweetId when merging enrichment. When omitted, use twitter.
platform?: 'twitter' | 'truth_social';
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 HistoryMedia = {
url: string;
type?: 'image' | 'video' | 'gif';
thumbnail?: string;
};
type TweetUrl = {
url: string;
name?: string;
tco?: string;
};
type TweetMention = {
handle?: string;
id?: string;
name?: string;
};
type HistoryTweetArticle = {
description?: string;
id?: string;
publishedAt?: number;
text?: string;
thumbnail?: string;
title?: string;
updatedAt?: number;
url?: string;
};
type HistoryTweetPollChoice = {
id?: string;
image?: string;
label?: string;
votes?: number;
};
type HistoryTweetPoll = {
choices: HistoryTweetPollChoice[];
endsAt?: number;
totalVotes?: number;
updatedAt?: number;
};
type TweetContentKind = 'post' | 'reply' | 'quote' | 'retweet';
type HistoryTweetReference = {
article?: HistoryTweetArticle;
type: 'reply' | 'quote' | 'retweet';
tweetId?: string;
text?: string;
translatedText?: string;
author?: TweetAuthor;
media?: HistoryMedia[];
poll?: HistoryTweetPoll;
quoted?: HistoryTweetReference;
subtweet?: HistoryTweetReference;
};
type TweetContent = {
tweetId: string;
kind: TweetContentKind;
// Original tweet text when the stored content includes both original and
// translated text.
text: string;
translatedText?: string;
createdAt: number;
author: TweetAuthor;
article?: HistoryTweetArticle;
link?: string;
media?: HistoryMedia[];
mentions?: TweetMention[];
poll?: HistoryTweetPoll;
// Epoch ms from the realtime payload when the stored content includes it.
receivedAt?: number;
urls?: TweetUrl[];
ref?: HistoryTweetReference;
};
type HistoricalContent =
| TweetContent
| ProfileUpdateEvent
| FollowEvent
| UnfollowEvent
| AffiliateUpdateEvent;
type HistoricalTweetResponse = {
tweetId: string;
twitterId: string;
twitterHandle: string | null;
body: string;
time: string;
// ISO TweetStream receipt time for the historical event.
receivedTime: string;
link: string;
messageType: 'TWEET' | 'PROFILE' | 'FOLLOW' | 'AFFILIATE';
content: HistoricalContent;
meta?: TweetMeta;
};
type HistoryResult = {
data: HistoricalTweetResponse[];
metadata: {
count: number;
nextCursor: string | null;
handle?: string;
handles?: string[];
startDate?: string;
endDate?: string;
type?: 'TWEET' | 'PROFILE' | 'FOLLOW' | 'AFFILIATE';
};
};History 响应
json{
"data": [
{
"tweetId": "account:affiliate:aff_01JQ8YQ5K8B8QKH6M0P8A1V2WX",
"twitterId": "123",
"twitterHandle": "organization",
"body": "Added New Member (@newmember) to affiliate list",
"time": "2025-04-09T00:00:00.123Z",
"receivedTime": "2025-04-09T00:00:00.123Z",
"link": "https://x.com/newmember",
"messageType": "AFFILIATE",
"content": {
"action": "added",
"eventId": "aff_01JQ8YQ5K8B8QKH6M0P8A1V2WX",
"observedAt": 1744156800123,
"receivedAt": 1744156800123,
"organization": {
"id": "123",
"handle": "@organization",
"name": "Organization",
"verifiedType": "business"
},
"member": {
"id": "456",
"handle": "@newmember",
"name": "New Member",
"profileImage": "https://pbs.twimg.com/profile_images/newmember_normal.jpg"
}
}
}
],
"metadata": {
"count": 1,
"nextCursor": null,
"type": "AFFILIATE"
}
}映射关联账号数据行
对于 AFFILIATE,顶层账号是 organization,content 与实时 affiliate_update 载荷一致。
处理错误
| 状态 | 响应 body | 含义 |
|---|---|---|
400 | { "error": "Invalid query parameters" } | 日期、limit、type 或账号格式错误 |
400 | { "error": "Invalid handle provided", "handle": "..." } | 账号校验失败 |
400 | { "error": "startDate must be before endDate" } | 日期范围顺序错误 |
400 | { "error": "Invalid cursor" } | cursor 格式错误或与其他筛选条件不匹配 |
401 | { "error": "Missing or invalid API key" } | Bearer token 缺失或格式错误 |
401 | { "error": "Invalid API key" } | Bearer token 格式正确,但不是有效 API 密钥 |
403 | { "error": "History is available on Pro, Scale, and Ultra Speed" } | 套餐不包含请求的 History 类型 |
403 | { "error": "Your subscription is not active", "message": "Please ensure your subscription is active", "status": "PAST_DUE" } | 订阅状态不允许使用 History |
403 | { "error": "Handle ... is not among your tracked accounts" } | 请求回放的账号不在监控列表中 |
429 | { "error": "Too many history requests", "retryAfterSeconds": 60 } | 超过请求频率限制 |