Skip to docs content

Envelope, delivery, recovery, and errors

Route the v1 envelope by family and operation, tolerate duplicates, and use stored history when a replay window is available.

Open AsyncAPI spec
Open in chat
Reviewed against product code on September 24, 2026

Route the envelope

Route every realtime envelope first by t, then by op. Keep handlers idempotent because history replay can return events already processed live.

Envelope base type

typescript
type EnvelopeBase<
  TFamily extends 'tweet' | 'account' | 'charity' | 'control' | 'news',
  TOp extends string,
  TPayload extends object,
> = {
  v: 1;
  t: TFamily;
  op: TOp;
  id?: string;
  ts: number;
  d: TPayload;
};

Route operations

TweetStream sends 18 server operations. Route each envelope by t, then op, and ignore unknown operations without closing the socket. Clients send two commands: auth_ping for an authentication check, and twitter_handles to add or remove accounts on a handle-management socket.

FamilyOperationMeaning
tweetcontentPost content
tweetmetaPost enrichment
tweetupdatePost update
tweetdeletePost deletion
tweetpinPost pinned
tweetunpinPost unpinned
accountprofile_updateProfile update
accountfollowAccount followed
accountunfollowAccount unfollowed
accountaffiliate_updateAffiliate membership update
accountcalloutToken callout
accountcallout_updateCallout enrichment
accountthesisFOMO thesis
accounttoken_verifiedFOMO token verification observed
charityaddedCharity added
newsarticleNews article
controlauth_pongAuthentication pong
controltwitter_handles_resultHandle command result

Treat delivery as an event stream

The envelope has no acknowledgement, resume token, or sequence number. Do not infer ordering or exactly-once delivery from ts or id.

Make handlers idempotent and keep enough local state to recognize events your application has already applied.

Backfill after reconnect

Choose a bounded startDate and endDate, follow nextCursor to null without changing the other filters, and process every returned row idempotently. Persist the window and cursor together if an interrupted replay must resume rather than restart.

History returns stored events; it does not promise completeness or a retention duration. AFFILIATE returns stored affiliate changes, not a current-list snapshot. Delete, pin, and unpin remain live-only events.

type HistoryRow = {
  time: string;
  tweetId: string;
};
 
type HistoryPage = {
  data: Array<HistoryRow>;
  metadata: { nextCursor: string | null };
};
 
async function replayAfterReconnect(
  checkpoint: string,
  processEvent: (event: HistoryRow) => Promise<void>,
  saveCheckpoint: (eventTime: string) => Promise<void>,
) {
  const apiKey = process.env.TWEETSTREAM_API_KEY;
  if (!apiKey) throw new Error("Missing TWEETSTREAM_API_KEY");
 
  const url = new URL("https://api.tweetstream.io/api/history");
  const replayEnd = new Date().toISOString();
  url.searchParams.set("handles", "marketdesk");
  url.searchParams.set("startDate", checkpoint);
  url.searchParams.set("endDate", replayEnd);
  url.searchParams.set("limit", "1000");
  url.searchParams.set("type", "TWEET");
 
  const pages: Array<Array<HistoryRow>> = [];
  let cursor: string | null = null;
  do {
    if (cursor) url.searchParams.set("cursor", cursor);
    const response = await fetch(url, {
      headers: { Authorization: `Bearer ${apiKey}` },
    });
    if (!response.ok) {
      const detail = await response.text();
      throw new Error(`Replay failed (${response.status}): ${detail}`);
    }
 
    const page = (await response.json()) as HistoryPage;
    pages.push(page.data);
    cursor = page.metadata.nextCursor;
  } while (cursor);
 
  for (const event of pages.reverse().flatMap((page) => [...page].reverse())) {
    await processEvent(event);
  }
  await saveCheckpoint(replayEnd);
}

Fix handshake errors

A failed WebSocket upgrade returns JSON before the socket is accepted. The client can safely log the response.

StatusLikely causeFix
400Missing or invalid WebSocket protocol headersSend tweetstream.v1 and a valid auth token protocol
401Missing, empty, or invalid API keyRegenerate or rotate the API key from the dashboard
403Subscription state does not allow live streamingStart a trial, renew, or upgrade the account
429Active WebSocket connection limit reachedClose an old socket or move to a higher plan
503Connection limit service temporarily unavailableRetry with backoff
{
  "error": "WebSocket connection limit reached (3/3 active). Close an existing connection and retry.",
  "status": 429
}