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`. Top-level `media` belongs to the current tweet; attachments from a quoted tweet appear only under `ref.media`.
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: 'video';
// Every video is a progressive MP4. A public still poster is included when available.
thumbnail?: string;
}
| {
type?: 'image' | 'gif';
thumbnail?: string;
}
);
type TweetUrl = {
url: string;
name?: string;
tco?: string;
};
type TweetMention = {
handle?: string;
id?: string;
name?: string;
};
type TweetArticle = {
description?: string;
id?: string;
publishedAt?: number;
text?: string;
thumbnail?: string;
title: string;
updatedAt?: number;
url: string;
};
type TweetPollChoice = {
id?: string;
image?: string;
label: string;
votes?: number;
};
type TweetPoll = {
choices: TweetPollChoice[];
endsAt?: number;
totalVotes?: number;
updatedAt?: number;
};
type TweetContentKind = 'post' | 'reply' | 'quote' | 'retweet';
type TweetReference = {
article?: TweetArticle;
type: 'reply' | 'quote' | 'retweet';
tweetId: string;
text?: string;
translatedText?: string;
author?: TweetAuthor;
media?: Media[];
poll?: TweetPoll;
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;
article?: TweetArticle;
link?: string;
media?: Media[];
mentions?: TweetMention[];
poll?: TweetPoll;
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 may arrive after tweet/content when additional tweet data becomes available. Every reference includes ref.tweetId; text, translated text, media, poll, and article fields are included when available. A text update with textUpdateType: 'completion' replaces an earlier truncated rendering of the same tweet and does not indicate an edit. Poll and article values are complete objects. Apply every update to the same tweet, replace the fields it includes, and keep existing values for fields it omits.
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: 'video';
// Every video is a progressive MP4. A public still poster is included when available.
thumbnail?: string;
}
| {
type?: 'image' | 'gif';
thumbnail?: string;
}
);
type TweetUrl = {
url: string;
name?: string;
tco?: string;
};
type TweetMention = {
handle?: string;
id?: string;
name?: string;
};
type TweetArticle = {
description?: string;
id?: string;
publishedAt?: number;
text?: string;
thumbnail?: string;
title: string;
updatedAt?: number;
url: string;
};
type TweetPollChoice = {
id?: string;
image?: string;
label: string;
votes?: number;
};
type TweetPoll = {
choices: TweetPollChoice[];
endsAt?: number;
totalVotes?: number;
updatedAt?: number;
};
type TweetContentKind = 'post' | 'reply' | 'quote' | 'retweet';
type TweetReference = {
article?: TweetArticle;
type: 'reply' | 'quote' | 'retweet';
tweetId: string;
text?: string;
translatedText?: string;
author?: TweetAuthor;
media?: Media[];
poll?: TweetPoll;
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;
article?: TweetArticle;
link?: string;
media?: Media[];
mentions?: TweetMention[];
poll?: TweetPoll;
receivedAt?: number;
urls?: TweetUrl[];
ref?: TweetReference;
};
type TweetUpdate = {
tweetId: string;
article?: TweetArticle;
kind?: TweetContentKind;
translatedText?: string;
author?: TweetAuthor;
media?: Media[];
mentions?: TweetMention[];
poll?: TweetPoll;
receivedAt?: number;
urls?: TweetUrl[];
ref?: TweetReference;
} & (
| {
text?: string;
textUpdateType?: never;
}
| {
text: string;
// Completes an earlier truncated rendering. This is not an edit signal.
textUpdateType: 'completion';
}
);{
"v": 1,
"t": "tweet",
"op": "update",
"id": "1234567890",
"ts": 1702500001800,
"d": {
"tweetId": "1234567890",
"kind": "reply",
"ref": {
"type": "reply",
"tweetId": "1234567880",
"text": "Parent tweet text"
}
}
}Lifecycle events
Deletes, pins, and unpins are explicit events. They should update downstream state, not overwrite the original content payload. A pin may include a non-rich tweet snapshot; when poll or article data is available, the pin is followed in per-tweet order by tweet/update carrying each complete object.
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: 'video';
// Every video is a progressive MP4. A public still poster is included when available.
thumbnail?: string;
}
| {
type?: 'image' | 'gif';
thumbnail?: string;
}
);
type TweetUrl = {
url: string;
name?: string;
tco?: string;
};
type TweetMention = {
handle?: string;
id?: string;
name?: string;
};
type TweetArticle = {
description?: string;
id?: string;
publishedAt?: number;
text?: string;
thumbnail?: string;
title: string;
updatedAt?: number;
url: string;
};
type TweetPollChoice = {
id?: string;
image?: string;
label: string;
votes?: number;
};
type TweetPoll = {
choices: TweetPollChoice[];
endsAt?: number;
totalVotes?: number;
updatedAt?: number;
};
type TweetContentKind = 'post' | 'reply' | 'quote' | 'retweet';
type TweetReference = {
article?: TweetArticle;
type: 'reply' | 'quote' | 'retweet';
tweetId: string;
text?: string;
translatedText?: string;
author?: TweetAuthor;
media?: Media[];
poll?: TweetPoll;
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;
article?: TweetArticle;
link?: string;
media?: Media[];
mentions?: TweetMention[];
poll?: TweetPoll;
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. Follow and unfollow payloads always include `target.handle`. Availability of other account fields 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 & {
handle: string;
};
};{
"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;
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<{ input: string; state: string }>;
};
};
const ws = new WebSocket("wss://ws-global.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);
}
});