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.
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;
};
{
"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.
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'>;
}>;
};
};
{
"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`.
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'];
};
{
"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.
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.
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;
};
{
"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.
{
"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"
}
}
}
{
"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"]
}
]
}
}
}
{
"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.
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.
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);
}
});