Skip to main content

Base Message Format

Every message sent over the websocket follows this base format:
export interface WebsocketWorkerEvent {
    id: string; // Unique ID for this event (use for debouncing and deduplication across multiple connections)
    type: string; // The type of event being sent
}

Tweet Events

export interface MiniTweetUpdate extends WebsocketWorkerEvent {
    type: 'tweet.mini.update';
    tweet: TwitterMiniTweet;
}
This is always the first event fired for any new tweet. It prioritizes speed over completeness, you get core tweet info instantly, but subtweet data (quotes, retweets, replies) won’t be populated yet.A tweet.update will follow shortly after if the tweet:
  • Is quoting, replying to, or retweeting another tweet
export interface TweetUpdate extends WebsocketWorkerEvent {
    type: 'tweet.update';
    tweet: TwitterTweet;
}
Sent a bit after tweet.mini.update with complete tweet data including resolved subtweets.Note: Subtweet chains are resolved up to 2 levels deep. If a chain is 3+ levels (e.g. a retweet of a reply to another tweet), levels 3+ will have minimal/default data. See tweet.full for fully resolved deep chains.
export interface TweetUpdateExpanded extends WebsocketWorkerEvent {
    type: 'tweet.update.expanded';
    tweet: TwitterTweet;
}
Same as tweet.update but with full untruncated text and article content if present.Only sent when:
  • The tweet text was truncated in tweet.update
  • The tweet contains an article
export interface TweetFull extends WebsocketWorkerEvent {
    type: 'tweet.full';
    tweet: TwitterTweet;
}
Sent when a tweet has a subtweet chain 3 or more levels deep. Every level in the chain is fully resolved with complete author data, metrics, media, and body text.Only triggered for chains 3+ levels deep. Standalone tweets, simple retweets, and single-level quotes/replies will not trigger this event.Timeline:
  • tweet.mini.update arrives first (fastest)
  • tweet.update arrives ~60ms later (levels 3+ may have empty data)
  • tweet.full arrives ~200-500ms later (all levels fully resolved)
Example chain (5 levels): 
Level 1: @elonmusk (QUOTE) "True"
  └─ Level 2: @DavidSacks (QUOTE) "And guess where those staffers went..."
      └─ Level 3: @beffjezos (QUOTE) "Here's the receipt..."
          └─ Level 4: @mattyglesias (QUOTE) "The hypothetical hypocrisy..."
              └─ Level 5: @beffjezos (TWEET) "Let's be real if the Dems had won..."
In tweet.update, levels 3-5 would have empty/default data. In tweet.full, every level has complete data.Supports up to 6 levels including the main tweet, we might increase it in the future, but 6 levels seems good for most use cases for now

Profile Events

export interface WorkerProfilePinnedUpdateEvent extends WebsocketWorkerEvent {
    type: 'profile.pinned.update';
    user: TwitterUser;
    pinned: TwitterTweet[];
}
export interface WorkerProfileUnpinnedUpdateEvent extends WebsocketWorkerEvent {
    type: 'profile.unpinned.update';
    user: TwitterUser;
    pinned: TwitterTweet[];
}
export interface ProfileUpdate extends WebsocketWorkerEvent {
    type: 'profile.update';
    user: TwitterUser;   // The new profile information
    before: TwitterUser; // The old profile information
}

Activity Events

export interface FollowingUpdate extends WebsocketWorkerEvent {
    type: 'following.update';
    change: 'unfollowed' | 'followed';
    following: TwitterUser; // The target user who was followed/unfollowed
    user: TwitterUser;      // The tracked user who performed the action
}
export interface DeletedTweet extends WebsocketWorkerEvent {
    type: 'tweet.deleted';
    tweet: TwitterTweet; // Metrics may be outdated (likes, replies, retweets, etc)
    deleted_at: number;  // UNIX timestamp in milliseconds
}
Don’t rely on metrics being accurate for deleted tweets — they reflect the last known values before deletion.

Types

TwitterUser

export interface TwitterUser {
    id: string;
    handle: string;
    private: boolean;
    verified: {
        type: 'none' | 'blue' | 'gold' | 'gray';
        label: null | {
            description: string;
            badge: null | string;
            url: null | string;
        };
    };
    sensitive: boolean;
    restricted: boolean;
    joined_at: number;

    profile: {
        name: string;
        location: null | string;
        avatar: null | string;
        banner: null | string;
        pinned: string[];
        url: null | {
            name: string;
            url: string;
            tco: string;
        };
        description: {
            text: string;
            urls: {
                name: string;
                url: string;
                tco: string;
            }[];
        };
    };

    metrics: {
        likes: number;
        media: number;
        tweets: number;
        friends: number;
        followers: number;
        following: number;
    };
}
FieldDescription
idThe user’s account ID
handleThe user’s handle (without @)
privateWhether the account is currently private
verified.typeblue = blue checkmark, gold = business/organization, gray = government/official, none = not verified
verified.labelAffiliated organization info (badge, description, profile link) — null if none
sensitiveWhether the account is flagged for sensitive content
restrictedWhether the account is restricted for unusual activity
joined_atUNIX timestamp in milliseconds
profile.pinnedArray of pinned tweet IDs
metrics.friendsMutual followers (people who follow the user and the user follows back)

TwitterMiniUser

export interface TwitterMiniUser {
    id: string;
    handle: string;
    verified: {
        type: 'none' | 'blue' | 'gold' | 'gray';
        label: null | {
            description: string;
            badge: null | string;
            url: null | string;
        };
    };
    profile: {
        name: string;
        avatar: null | string;
    };
    metrics: {
        following: number;
        followers: number;
    };
}

TwitterTweet

export interface TwitterTweet {
    id: string;
    type: 'TWEET' | 'RETWEET' | 'QUOTE' | 'REPLY';
    created_at: number;
    author: TwitterUser;
    subtweet: null | TwitterTweet;

    reply: null | {
        id: string;
        handle: string;
    };

    quoted: null | {
        id: string;
        handle: string;
    };

    body: {
        text: string;
        urls: {
            name: string;
            url: string;
            tco: string;
        }[];
        mentions: {
            id: string;
            name: string;
            handle: string;
        }[];
        components: (
            | { type: 'text'; text: string; bold: boolean; italics: boolean; }
            | { type: 'image'; url: string; }
            | { type: 'video'; url: string; }
        )[];
    };

    media: {
        images: string[];
        videos: string[];
        thumbnails: string[];
        proxied: null | {
            images: string[];
            thumbnails: string[];
        };
    };

    grok: null | {
        id: string;
        conversation: {
            from: 'USER' | 'AGENT';
            message: string;
            images: string[];
        }[];
    };

    card: null | {
        url: string;
        image: string;
        title: string;
        description: string;
    };

    poll: null | {
        ends_at: number;
        updated_at: number;
        choices: {
            label: string;
            count: number;
        }[];
    };

    article: null | {
        id: string;
        title: string;
        thumbnail: null | string;
        created_at: number;
        updated_at: number;
        body: {
            text: string;
            components: (
                | { type: 'divider'; }
                | {
                    type: 'text';
                    variant:
                        | 'header-one'
                        | 'header-two'
                        | 'paragraph'
                        | 'blockquote'
                        | 'ordered-list'
                        | 'unordered-list'
                        | 'latex-box'
                        | 'markdown-box';
                    lines: {
                        text: string;
                        styles: {
                            from: number;
                            to: number;
                            text: 'bold' | 'italics' | 'strikethrough';
                        }[];
                        urls: {
                            from: number;
                            to: number;
                            url: string;
                        }[];
                    }[];
                }
                | {
                    type: 'media';
                    variant: 'image' | 'gif' | 'video';
                    url: string;
                    thumbnail: string;
                    caption: null | string;
                }
                | {
                    type: 'tweet';
                    tweet: {
                        id: string;
                        url: string;
                        object: null | TwitterTweet;
                    };
                }
            )[];
        };
    };

    community: null | object;

    metrics: {
        likes: number;
        quotes: number;
        replies: number;
        retweets: number;
        advanced: null | {
            views: number;
        };
    };
}
FieldDescription
subtweetThe nested tweet this tweet references (reply parent, quoted tweet, or retweeted tweet). Recursive — can contain its own subtweet.
replyIf this is a REPLY, contains the ID and handle of the tweet being replied to
quotedIf this is a QUOTE, contains the ID and handle of the tweet being quoted
body.componentsRich text components for expanded/long tweets — only populated in tweet.update.expanded
media.proxiedProxied media URLs — not always available, fall back to normal URLs
grokEmbedded Grok AI conversation preview, if the tweet contains one
cardURL card preview (image, title, description) for linked articles
articleFull article content if the tweet is a Twitter article
metrics.advancedView count — null until expanded version is fetched

TwitterMiniTweet

export interface TwitterMiniTweet {
    id: string;
    type: 'TWEET' | 'RETWEET' | 'QUOTE' | 'REPLY';
    created_at: number;
    author: TwitterMiniUser;
    subtweet: null | TwitterMiniTweet;

    reply: null | {
        id: string;
        handle: string;
    };

    quoted: null | {
        id: string;
        handle: string;
    };

    body: {
        text: string;
        urls: {
            name: string;
            url: string;
            tco: string;
        }[];
        mentions: {
            id: string;
            name: string;
            handle: string;
        }[];
    };

    media: {
        images: string[];
        videos: string[];
        thumbnails: string[];
        proxied: null | {
            images: string[];
        };
    };
}

Event Flow

Here’s the order events fire for different tweet types:
Standalone tweet:
  1. tweet.mini.update  (instant)
  2. tweet.update        (a tiny bit later, only if has URL/card)

Quote / Reply / Retweet (2 levels):
  1. tweet.mini.update  (instant)
  2. tweet.update        (a tiny bit later, subtweet fully resolved)

Deep chain (3+ levels):
  1. tweet.mini.update  (instant)
  2. tweet.update        (a tiny bit later, levels 3+ have minimal data)
  3. tweet.full          (a little more later, ALL levels fully resolved)
We are always working on new events — keep an eye on this section for updates!