---
title: "History API"
canonical_url: "https://tweetstream.io/zh-cn/docs/history-api"
last_updated: 2026-09-24
---

# History API

通过 History API 回放已存内容、资料、关注和关联账号事件。

Canonical URL: https://tweetstream.io/zh-cn/docs/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 请求

```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) {
  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 响应类型

```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 }` | 超过请求频率限制 |
