Skip to docs content

History API

Replay stored content, profile, follow, and affiliate events through the History API.

Open OpenAPI spec
Open in chat
Reviewed against product code on September 24, 2026

Request history

  • GET/api/history

Use GET /api/history to replay stored events after reconnecting or inspect a specific time window.

Check access

Pro, Scale, and Ultra Speed can request every History event type. Standard keys query base-plan tracked accounts; Ultra keys query selected Ultra accounts. Supported types are TWEET, PROFILE, FOLLOW, and AFFILIATE. Delete, pin, and unpin remain live-only events.

Set filters

Without a handle filter, History searches all active accounts tracked by the key. A filter can include only handles that key currently tracks.

ParameterRequiredNotes
handle, handles, handle[], handles[]NoOne handle, repeated handles, or comma-separated handles
startDateNoISO datetime lower bound
endDateNoISO datetime upper bound
limitNoDefaults to 100; maximum 1000
typeNoTWEET, PROFILE, FOLLOW, or AFFILIATE; case-insensitive. Defaults to TWEET
cursorNoOpaque nextCursor from the previous page; keep every other filter unchanged

History request

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

Page through a bounded window

History returns up to 1000 rows per request in stable newest-first order.

Set both bounds

Capture one endDate, choose a startDate, and keep both values unchanged for every page in the replay.

Continue with nextCursor

Send each returned nextCursor with the same type, handles, and date bounds. A null cursor ends the window. Process rows idempotently and keep live lifecycle handlers active because History replays stored event families separately from live-only state changes.

Read the response

Read the rows and request metadata before advancing the replay window.

Read rows and metadata

Results use stable newest-first traversal. metadata.count reports the returned rows, metadata.nextCursor continues the same filtered query, and a null cursor ends the result set. Older stored FOLLOW rows can have an empty target; treat the target identity as unavailable and continue processing the event.

History response types

Show full definitions (242 lines)
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';
  };
};
{
  "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"
  }
}

Map affiliate rows

For AFFILIATE, the top-level account is the organization and content matches the live affiliate_update payload.

Handle errors

StatusBodyMeaning
400{ "error": "Invalid query parameters" }Malformed dates, limit, type, or handle
400{ "error": "Invalid handle provided", "handle": "..." }Handle validation failed
400{ "error": "startDate must be before endDate" }Date range is reversed
400{ "error": "Invalid cursor" }Cursor is malformed or does not match the other filters
401{ "error": "Missing or invalid API key" }Missing or malformed bearer token
401{ "error": "Invalid API key" }Bearer token was well-formed but does not match an active API key
403{ "error": "History is available on Pro, Scale, and Ultra Speed" }Plan does not include the requested History type
403{ "error": "Your subscription is not active", "message": "Please ensure your subscription is active", "status": "PAST_DUE" }Subscription state does not allow history
403{ "error": "Handle ... is not among your tracked accounts" }Requested replay handle is outside your watchlist
429{ "error": "Too many history requests", "retryAfterSeconds": 60 }Rate limit exceeded