Skip to docs content
Docs

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.

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' | '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;
};
1. Reply contentjson
{
  "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.

Typetypescript
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'>;
    }>;
  };
};
Examplejson
{
  "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.

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' | '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';
    }
);
2. Direct referencejson
{
  "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."
    }
  }
}
3. Deeper reference chainjson
{
  "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.

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' | '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.

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' | '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;
};
Profile update 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"
    }
  }
}
Affiliate list change examplejson
{
  "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.

Typetypescript
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;
};
Examplejson
{
  "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.

Typetypescript
type CharityAddedEvent = {
  id: string;
  name: string;
  slug: string;
  enabled: boolean;
  taxId?: string;
  location?: {
    city?: string;
    state?: string;
    country?: string;
  };
  observedAt: number;
};
Examplejson
{
  "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.

EndpointMethodUse
/api/news/sourcesGETList publication availability and your settings
/api/news/sourcesPATCHTurn one available publication on or off
/api/news/historyGETRead stored articles from enabled publications
Article typetypescript
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;
};
WebSocket articlejson
{
  "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"
  }
}
History typetypescript
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;
  };
};
List publicationstypescript
const response = await fetch(
  "https://api.tweetstream.io/api/news/sources",
  {
    headers: {
      Authorization: `Bearer ${process.env.TWEETSTREAM_API_KEY}`,
    },
  },
);
 
console.log(await response.json());
Publication responsejson
{
  "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
    }
  ]
}
Enable a publicationtypescript
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());
Read news historytypescript
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);
}
History responsejson
{
  "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.

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",
    "platform": "twitter",
    "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",
    "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.

Typetypescript
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.

Follow handles over WebSockettypescript
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);
  }
});