Skip to docs content
Docs

Payloads and events

TweetStream sends compact JSON envelopes. Content carries the social event; meta carries enrichment; lifecycle and account events are separate operations.

Tweet content

`tweet/content` is the first event most consumers handle. `author.platform` is `twitter` or `truth_social`. When `partial` is true, the upstream supplied truncated text; TweetStream removes any same-post continuation link instead of presenting it as content.

Typetypescript
type VerifiedType = 'blue' | 'business' | 'government' | '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';
  profileImage?: string;
  url?: string;
  verifiedLabel?: TweetVerifiedLabel;
  verifiedType?: VerifiedType;
};
 
type Media = {
  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 TweetContentKind = 'post' | 'reply' | 'quote' | 'retweet';
 
type TweetReference = {
  type: 'reply' | 'quote' | 'retweet';
  tweetId?: string;
  text?: string;
  translatedText?: string;
  author?: TweetAuthor;
  media?: Media[];
  // True when the upstream only supplied truncated text.
  partial?: boolean;
  quoted?: 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;
  link?: string;
  media?: Media[];
  mentions?: TweetMention[];
  // True when text is known to be truncated.
  partial?: boolean;
  receivedAt?: number;
  urls?: TweetUrl[];
  ref?: TweetReference;
};
 
Examplejson
{
  "v": 1,
  "t": "tweet",
  "op": "content",
  "id": "1234567890",
  "ts": 1702500000130,
  "d": {
    "tweetId": "1234567890",
    "kind": "post",
    "text": "新项目已上线。合约地址:9xQeWvG816bUx9EP...",
    "translatedText": "New project is live. Contract address: 9xQeWvG816bUx9EP...",
    "createdAt": 1702500000000,
    "receivedAt": 1702500000123,
    "author": {
      "id": "123456",
      "handle": "@marketdesk",
      "name": "Market Desk",
      "platform": "twitter",
      "followersCount": 125000,
      "verifiedType": "business"
    },
    "link": "https://x.com/marketdesk/status/1234567890"
  }
}
 

Enrichment metadata

`tweet/meta` is keyed by tweetId and can arrive after content. Merge it into your local tweet row instead of treating it as a separate alert unless your workflow wants enrichment-only alerts.

Typetypescript
type TweetMeta = {
  tweetId: string;
  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'>;
    }>;
  };
};
 
Examplejson
{
  "v": 1,
  "t": "tweet",
  "op": "meta",
  "id": "1234567890",
  "ts": 1702500001000,
  "d": {
    "tweetId": "1234567890",
    "ocr": {
      "text": "Chart showing SOL breakout at $100"
    },
    "detected": {
      "tokens": [
        {
          "symbol": "SOL",
          "name": "Solana",
          "priceUsd": 98.50,
          "sources": ["text", "ocr"]
        }
      ]
    }
  }
}
 

Tweet updates

`tweet/update` carries an incremental update for a known tweetId. Merge defined fields into the existing record and preserve earlier fields that are absent from the update payload. A `partial: true` update marks its text as truncated; a later text update without that flag replaces it with complete text. Translations may arrive with either `tweet/content` or `tweet/update` as `translatedText`.

Typetypescript
type VerifiedType = 'blue' | 'business' | 'government' | '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';
  profileImage?: string;
  url?: string;
  verifiedLabel?: TweetVerifiedLabel;
  verifiedType?: VerifiedType;
};
 
type Media = {
  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 TweetContentKind = 'post' | 'reply' | 'quote' | 'retweet';
 
type TweetReference = {
  type: 'reply' | 'quote' | 'retweet';
  tweetId?: string;
  text?: string;
  translatedText?: string;
  author?: TweetAuthor;
  media?: Media[];
  // True when the upstream only supplied truncated text.
  partial?: boolean;
  quoted?: 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;
  link?: string;
  media?: Media[];
  mentions?: TweetMention[];
  // True when text is known to be truncated.
  partial?: boolean;
  receivedAt?: number;
  urls?: TweetUrl[];
  ref?: TweetReference;
};
 
type TweetUpdate = {
  tweetId: string;
  kind?: TweetContentKind;
  text?: string;
  translatedText?: string;
  author?: TweetAuthor;
  media?: Media[];
  mentions?: TweetMention[];
  partial?: boolean;
  receivedAt?: number;
  urls?: TweetUrl[];
  ref?: TweetContent['ref'];
};
 
Examplejson
{
  "v": 1,
  "t": "tweet",
  "op": "update",
  "id": "1234567890",
  "ts": 1702500001800,
  "d": {
    "tweetId": "1234567890",
    "translatedText": "New project is live. Contract address: 9xQeWvG816bUx9EP..."
  }
}
 

Lifecycle events

Deletes, pins, and unpins are explicit events. They should update downstream state, not overwrite the original content payload.

Typestypescript
type VerifiedType = 'blue' | 'business' | 'government' | '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';
  profileImage?: string;
  url?: string;
  verifiedLabel?: TweetVerifiedLabel;
  verifiedType?: VerifiedType;
};
 
type Media = {
  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 TweetContentKind = 'post' | 'reply' | 'quote' | 'retweet';
 
type TweetReference = {
  type: 'reply' | 'quote' | 'retweet';
  tweetId?: string;
  text?: string;
  translatedText?: string;
  author?: TweetAuthor;
  media?: Media[];
  // True when the upstream only supplied truncated text.
  partial?: boolean;
  quoted?: 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;
  link?: string;
  media?: Media[];
  mentions?: TweetMention[];
  // True when text is known to be truncated.
  partial?: boolean;
  receivedAt?: number;
  urls?: TweetUrl[];
  ref?: TweetReference;
};
 
type TweetDeleteEvent = {
  tweetId: string;
  eventId: string;
  deletedAt?: number;
  receivedAt?: number;
  author?: TweetAuthor;
  text?: string;
};
 
type TweetPinEvent = {
  tweetId: string;
  eventId: string;
  observedAt: number;
  receivedAt?: number;
  action: 'pin' | 'unpin';
  author: TweetAuthor;
  text?: string;
  tweet?: TweetContent;
};
 

Account events

Profile, follow, and unfollow events use the `account` family. Availability depends on what is observed for the monitored account.

Typestypescript
type VerifiedType = 'blue' | 'business' | 'government' | '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';
  profileImage?: string;
  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' | 'UNFOLLOW';
  eventId: string;
  observedAt: number;
  receivedAt?: number;
  actor: AccountEventActor;
  target: AccountEventActor;
};
 
Examplejson
{
  "v": 1,
  "t": "account",
  "op": "profile_update",
  "ts": 1744156801000,
  "d": {
    "kind": "PROFILE",
    "eventId": "profile_1",
    "observedAt": 1744156800000,
    "receivedAt": 1744156800123,
    "actor": {
      "id": "123",
      "handle": "@tracked",
      "name": "Tracked Account",
      "profileImage": "https://pbs.twimg.com/profile_images/new-avatar_normal.jpg",
      "followersCount": 125000,
      "followingCount": 321,
      "verifiedType": "business",
      "verifiedLabel": {
        "badge": "https://pbs.twimg.com/affiliation_badge.jpg",
        "description": "Example Org",
        "url": "https://x.com/example"
      },
      "websiteUrl": "https://tracked.example",
      "location": "New York, NY"
    },
    "changes": {
      "avatar": "https://pbs.twimg.com/profile_images/new-avatar_normal.jpg",
      "bio": "Now watching markets 24/7",
      "websiteUrl": "https://new-site.example"
    },
    "previous": {
      "avatar": "https://pbs.twimg.com/profile_images/old-avatar_normal.jpg",
      "bio": "Old bio",
      "websiteUrl": "https://old-site.example"
    }
  }
}
 

Typical sequence

A post with an image usually arrives in phases. Treat `content` as the alertable event, merge later `meta` into the same local row, and process lifecycle events as state changes.

1. Contentjson
{
  "v": 1,
  "t": "tweet",
  "op": "content",
  "id": "2064689031777615872",
  "ts": 1781095200123,
  "d": {
    "tweetId": "2064689031777615872",
    "kind": "post",
    "text": "New token live. CA: 9xQeWvG816bUx9EP...",
    "createdAt": 1781095199900,
    "receivedAt": 1781095200108,
    "author": {
      "handle": "@marketdesk",
      "name": "Market Account",
      "platform": "twitter"
    }
  }
}
 
2. Enrichmentjson
{
  "v": 1,
  "t": "tweet",
  "op": "meta",
  "id": "2064689031777615872",
  "ts": 1781095200340,
  "d": {
    "tweetId": "2064689031777615872",
    "detected": {
      "tokens": [
        {
          "symbol": "EDGE",
          "contract": "9xQeWvG816bUx9EP...",
          "chain": "solana",
          "priceUsd": 0.0042,
          "sources": ["text", "ocr"]
        }
      ]
    }
  }
}
 
3. Lifecyclejson
{
  "v": 1,
  "t": "tweet",
  "op": "delete",
  "id": "2064689031777615872",
  "ts": 1781095300123,
  "d": {
    "tweetId": "2064689031777615872",
    "eventId": "delete_2064689031777615872",
    "deletedAt": 1781095300100
  }
}
 

Handle management results

WebSocket handle-management commands return `control/twitter_handles_result`. REST add/remove endpoints expose the same row-level state model for backend workflows.

Typetypescript
type TwitterHandlesResult = {
  action: 'follow' | 'unfollow';
  requestId: string | null;
  results: Array<{
    input: string;
    handle?: string;
    name?: string;
    normalizedHandle?: string;
    profileImage?: string;
    twitterId?: string;
    state:
      | 'added'
      | 'already_following'
      | 'invalid_input'
      | 'duplicate'
      | 'not_found'
      | 'failed'
      | 'removed'
      | 'not_following';
    message?: string;
  }>;
  error: string | null;
};
 

Manage handles over WebSocket

Use the handle-management subprotocol when a long-running backend socket should add or remove monitored accounts without a separate REST request.

Follow handles over WebSockettypescript
import WebSocket from "ws";
 
type HandleManagementEvent = {
  t?: string;
  op?: string;
  d?: {
    error?: string | null;
    results?: Array<{ handle?: string; input: string; state: string }>;
  };
};
 
const ws = new WebSocket("wss://ws.tweetstream.io/ws", [
  "tweetstream.handle-management",
  `tweetstream.auth.token.${process.env.TWEETSTREAM_API_KEY}`,
]);
 
ws.on("open", () => {
  ws.send(JSON.stringify({
    type: "twitter_handles",
    action: "follow",
    handles: ["marketdesk", "realDonaldTrump"],
    requestId: crypto.randomUUID(),
  }));
});
 
ws.on("message", (raw) => {
  const event = JSON.parse(raw.toString()) as HandleManagementEvent;
  if (event.t === "control" && event.op === "twitter_handles_result") {
    console.log(event.d?.results, event.d?.error);
  }
});