Payloads and events
TweetStream sends compact JSON envelopes. Content contains the social event, meta contains enrichment, and lifecycle and account events use separate operations.
Tweet content
Handle tweet/content immediately. author.platform is twitter, truth_social, or binance_square; use twitter when it is omitted. Binance Square content uses the same envelope and puts the Square post ID in tweetId. Its kind is post, reply, or quote. A reply, quote, or repost may already include ref; otherwise it can arrive in a later tweet/update. For X/Twitter, deeper references use ref.subtweet. Top-level media belongs to the current post; referenced attachments stay 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' | 'binance_square';
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;
subtweet?: 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": "reply",
"text": "Could this launch today?",
"createdAt": 1702500000000,
"author": {
"handle": "@marketdesk",
"platform": "twitter"
}
}
}Enrichment metadata
tweet/meta can arrive after content. Merge it by (platform, tweetId), not by tweetId alone. New frames include platform; treat an omitted value as twitter when reading legacy X frames.
type TweetMeta = {
tweetId: string;
// Pair with tweetId when merging enrichment. Omitted means twitter on legacy frames.
platform?: 'twitter' | 'truth_social' | 'binance_square';
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",
"platform": "twitter",
"ocr": {
"text": "Chart showing SOL breakout at $100"
},
"detected": {
"tokens": [
{
"symbol": "SOL",
"name": "Solana",
"priceUsd": 98.50,
"sources": ["text", "ocr"]
}
]
}
}
}Tweet updates
Handle tweet/content immediately. It may already include ref; otherwise one or more tweet/update frames can follow. The examples below show one possible sequence. Apply each update to the same (platform, tweetId). When an update includes ref, replace the stored reference with that snapshot. ref.tweetId identifies the direct reference, and each nested subtweet has its own tweetId. X/Twitter chains can include up to seven referenced posts when available, including the direct reference. quoted is another nested reference field kept for compatibility. If quoted and subtweet identify the same post, keep one copy by tweetId. Reference text, translations, media, polls, and articles are optional and may be added in a later update. For other update fields, replace only those present. A completion text update replaces truncated text and is not an edit. Poll and article values are complete objects.
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' | 'binance_square';
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;
subtweet?: 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;
// Pair with tweetId. On legacy frames, fall back to author.platform, then twitter.
platform?: 'twitter' | 'truth_social' | 'binance_square';
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": 1702500000180,
"d": {
"tweetId": "1234567890",
"platform": "twitter",
"ref": {
"type": "reply",
"tweetId": "1234567880",
"text": "Mainnet is ready for launch."
}
}
}{
"v": 1,
"t": "tweet",
"op": "update",
"id": "1234567890",
"ts": 1702500000240,
"d": {
"tweetId": "1234567890",
"platform": "twitter",
"ref": {
"type": "reply",
"tweetId": "1234567880",
"text": "Mainnet is ready for launch.",
"author": {
"handle": "@projectteam",
"name": "Project Team"
},
"subtweet": {
"type": "quote",
"tweetId": "1234567800",
"text": "Launch proposal and contract details.",
"author": {
"handle": "@projectteam",
"name": "Project Team"
}
}
}
}
}Lifecycle events
Delete, pin, and unpin events update the post identified by (platform, tweetId); they do not overwrite the original content payload. A pin may include a tweet snapshot without rich content. When poll or article data is available, the pin is followed in per-post 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' | 'binance_square';
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;
subtweet?: 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;
// On legacy frames, fall back to author.platform, then twitter.
platform?: 'twitter' | 'truth_social' | 'binance_square';
eventId: string;
deletedAt?: number;
receivedAt?: number;
author?: TweetAuthor;
text?: string;
};
type TweetPinEvent = {
tweetId: string;
// On legacy frames, fall back to tweet.author.platform or author.platform, then twitter.
platform?: 'twitter' | 'truth_social' | 'binance_square';
eventId: string;
observedAt: number;
receivedAt?: number;
action: 'pin' | 'unpin';
author: TweetAuthor;
text?: string;
tweet?: TweetContent;
};Account events
Profile, follow, unfollow, and affiliate list changes use the account family. Follow and unfollow payloads include target.handle. For affiliate_update, store (organization.id, member.id) and apply the latest action. Use eventId only to deduplicate an exact replay. Keyword filters do not apply. Replay recorded changes with GET /api/history?type=affiliate; there is no current-list snapshot or pre-feature backfill. Ignore or log unknown account operations without closing the connection.
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' | 'binance_square';
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;
};
};
type AffiliateAccountIdentity = AccountEventActor & {
id: string;
};
type AffiliateUpdateEvent = {
action: 'added' | 'removed';
eventId: string;
observedAt: number;
receivedAt?: number;
organization: AffiliateAccountIdentity;
member: AffiliateAccountIdentity;
};{
"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"
}
}
}{
"v": 1,
"t": "account",
"op": "affiliate_update",
"id": "aff_01JQ8YQ5K8B8QKH6M0P8A1V2WX",
"ts": 1744156800150,
"d": {
"action": "added",
"eventId": "aff_01JQ8YQ5K8B8QKH6M0P8A1V2WX",
"observedAt": 1744156800000,
"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"
}
}
}Callouts
Follow pump.fun callouts from up to 10 accounts. In the Callouts tab, add an account using a pump.fun username, Solana wallet, or profile URL. Labels are optional; without one, we show the pump.fun username. Events use account/callout. Route them by d.platform; pump.fun uses pump_fun. d.caller.username and d.token.address are always present. Token name, symbol, artwork, URL, market cap, prices, and multiple are included when available. d.token.image is token artwork, not an account profile image. Use calloutId to match the callout on pump.fun. Use the envelope id to deduplicate TweetStream replays. d.createdAt is the pump.fun creation time. d.receivedAt, when present, is when TweetStream received it. Envelope ts is when the event was prepared for delivery.
type CalloutEvent = {
platform: 'pump_fun';
caller: {
username: string;
};
token: {
address: string;
symbol?: string;
name?: string;
// Token artwork when available. This is not an account profile image.
image?: string;
};
url?: string;
marketCapUsd?: number;
// Exact decimal strings preserve small onchain values.
calloutPrice?: string;
multiple?: number;
maxPriceSol?: string;
// The pump.fun Callout UUID.
calloutId: string;
// When pump.fun created the Callout.
createdAt: number;
// When TweetStream received it.
receivedAt?: number;
};{
"v": 1,
"t": "account",
"op": "callout",
"id": "callout.0123456789abcdef0123",
"ts": 1772000001060,
"d": {
"platform": "pump_fun",
"caller": {
"username": "CryptoCalls"
},
"token": {
"address": "So11111111111111111111111111111111111111112",
"symbol": "ABC",
"name": "Alpha Beta",
"image": "https://cdn.example/token.png"
},
"url": "https://pump.fun/coin/So11111111111111111111111111111111111111112",
"marketCapUsd": 156230.75,
"calloutPrice": "0.00000125",
"multiple": 2.5,
"maxPriceSol": "0.00000450",
"calloutId": "9ab3177c-7b35-4d99-9cca-8b426f74a270",
"createdAt": 1772000001000,
"receivedAt": 1772000001050
}
}Charity events
Turn on charity alerts in the dashboard. New charities arrive over WebSocket. Discord alerts are best effort and use your global webhook. Keyword filters do not apply. Use id to ignore an exact replay. History does not store these events.
type CharityAddedEvent = {
id: string;
name: string;
slug: string;
enabled: boolean;
taxId?: string;
location?: {
city?: string;
state?: string;
country?: string;
};
observedAt: number;
};{
"v": 1,
"t": "charity",
"op": "added",
"id": "4ffccf6e-66d4-555a-af5d-ceb36c10e3f0",
"ts": 1772000001060,
"d": {
"id": "4ffccf6e-66d4-555a-af5d-ceb36c10e3f0",
"name": "Council on Foreign Relations",
"slug": "council-on-foreign-relations",
"enabled": true,
"taxId": "13-1628168",
"location": {
"city": "New York City",
"state": "NY",
"country": "US"
},
"observedAt": 1772000001060
}
}News articles
Enable publications in the dashboard or with PATCH /api/news/sources. Articles arrive as news/article events on the same WebSocket. Setting changes apply automatically. Each source has a stable TweetStream ID and publication name. GET /api/news/history returns articles from enabled publications. Set before to the previous nextCursor or an ISO date. Results are ordered by receipt time, newest first. limit defaults to 50 and accepts 1 through 100. A page may contain fewer results when it reaches the response size limit. Continue with nextCursor.
| Endpoint | Method | Use |
|---|---|---|
| /api/news/sources | GET | List publication availability and your settings |
| /api/news/sources | PATCH | Turn one available publication on or off |
| /api/news/history | GET | Read stored articles from enabled publications |
type NewsSource = {
id: string;
name: string;
};
type NewsMedia = {
url: string;
type?: 'image' | 'video';
caption?: string;
};
type NewsArticle = {
source: NewsSource;
url: string;
title: string;
publishedAt: string;
receivedAt: number;
modifiedAt?: string;
primaryCategory?: string;
categories: string[];
author?: string;
keywords: string[];
description?: string;
summary?: string;
media: NewsMedia[];
content?: string;
language?: string;
copyright?: string;
};{
"v": 1,
"t": "news",
"op": "article",
"id": "news_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"ts": 1787786858123,
"d": {
"source": {
"id": "bbc-news",
"name": "BBC News"
},
"url": "https://www.bbc.com/news/articles/example",
"title": "Markets open after the holiday",
"publishedAt": "2026-08-26T23:27:38.000Z",
"receivedAt": 1787786858123,
"primaryCategory": "Business",
"categories": ["Business"],
"author": "BBC News",
"keywords": ["markets"],
"description": "A brief description supplied by the publication.",
"summary": "Markets reopened after the holiday.",
"media": [
{
"url": "https://ichef.bbci.co.uk/news/example.jpg",
"type": "image",
"caption": "A market floor"
}
],
"content": "The stored article text, when available.",
"language": "en",
"copyright": "BBC"
}
}type NewsSource = {
id: string;
name: string;
};
type NewsMedia = {
url: string;
type?: 'image' | 'video';
caption?: string;
};
type NewsArticle = {
source: NewsSource;
url: string;
title: string;
publishedAt: string;
receivedAt: number;
modifiedAt?: string;
primaryCategory?: string;
categories: string[];
author?: string;
keywords: string[];
description?: string;
summary?: string;
media: NewsMedia[];
content?: string;
language?: string;
copyright?: string;
};
type HistoricalNewsArticle = {
id: string;
source: NewsSource;
url: string;
title: string;
publishedAt: string;
receivedAt: string;
modifiedAt: string | null;
primaryCategory: string | null;
categories: string[];
author: string | null;
keywords: string[];
description: string | null;
summary: string | null;
media: NewsMedia[];
content: string | null;
language: string | null;
copyright: string | null;
};
type NewsHistoryResult = {
data: HistoricalNewsArticle[];
metadata: {
count: number;
nextCursor: string | null;
sourceId?: string;
};
};const response = await fetch(
"https://api.tweetstream.io/api/news/sources",
{
headers: {
Authorization: `Bearer ${process.env.TWEETSTREAM_API_KEY}`,
},
},
);
console.log(await response.json());{
"sources": [
{
"id": "bbc-news",
"name": "BBC News",
"icon": "/news-sources/bbc.png",
"available": true,
"enabled": true
},
{
"id": "dexerto",
"name": "Dexerto",
"icon": "/news-sources/dexerto.png",
"available": false,
"enabled": false
}
]
}const response = await fetch(
"https://api.tweetstream.io/api/news/sources",
{
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.TWEETSTREAM_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
sourceId: "bbc-news",
enabled: true,
}),
},
);
console.log(await response.json());const url = new URL(
"https://api.tweetstream.io/api/news/history",
);
url.searchParams.set("sourceId", "bbc-news");
url.searchParams.set("limit", "1");
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${process.env.TWEETSTREAM_API_KEY}`,
},
});
const page = await response.json();
console.log(page.data);
if (page.metadata.nextCursor) {
url.searchParams.set("before", page.metadata.nextCursor);
}{
"data": [
{
"id": "news_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"source": {
"id": "bbc-news",
"name": "BBC News"
},
"url": "https://www.bbc.com/news/articles/example",
"title": "Markets open after the holiday",
"publishedAt": "2026-08-26T23:27:38.000Z",
"receivedAt": "2026-08-26T23:27:38.123Z",
"modifiedAt": null,
"primaryCategory": "Business",
"categories": ["Business"],
"author": "BBC News",
"keywords": ["markets"],
"description": "A brief description supplied by the publication.",
"summary": "Markets reopened after the holiday.",
"media": [
{
"url": "https://ichef.bbci.co.uk/news/example.jpg",
"type": "image",
"caption": "A market floor"
}
],
"content": "The stored article text, when available.",
"language": "en",
"copyright": "BBC"
}
],
"metadata": {
"count": 1,
"nextCursor": "v1.MTc4Nzc4Njg1ODEyMwpuZXdzXzAxMjM0NTY3ODlhYmNkZWYwMTIzNDU2Nzg5YWJjZGVmMDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY",
"sourceId": "bbc-news"
}
}Typical sequence
A post with an image can arrive in stages. Treat content as the alert, then use (platform, tweetId) to merge later meta and apply lifecycle events to the same local row.
{
"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",
"platform": "twitter",
"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",
"platform": "twitter",
"eventId": "delete_2064689031777615872",
"deletedAt": 1781095300100
}
}Handle management results
WebSocket handle-management commands return control/twitter_handles_result. The REST add and remove endpoints return the same per-handle states.
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 to add or remove monitored accounts through a long-running backend socket instead of 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);
}
});