---
title: "Envelope, delivery, recovery, and errors"
canonical_url: "https://tweetstream.io/docs/envelope-delivery-recovery-errors"
last_updated: 2026-09-24
---

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

Canonical URL: https://tweetstream.io/docs/envelope-delivery-recovery-errors

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

- [Open the complete router](https://tweetstream.io/docs/production-consumer#complete-router): Every payload type and an exhaustive `switch` over all server operations.

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

| Family | Operation | Meaning |
| --- | --- | --- |
| `tweet` | `content` | Post content |
| `tweet` | `meta` | Post enrichment |
| `tweet` | `update` | Post update |
| `tweet` | `delete` | Post deletion |
| `tweet` | `pin` | Post pinned |
| `tweet` | `unpin` | Post unpinned |
| `account` | `profile_update` | Profile update |
| `account` | `follow` | Account followed |
| `account` | `unfollow` | Account unfollowed |
| `account` | `affiliate_update` | Affiliate membership update |
| `account` | `callout` | Token callout |
| `account` | `callout_update` | Callout enrichment |
| `account` | `thesis` | FOMO thesis |
| `account` | `token_verified` | FOMO token verification observed |
| `charity` | `added` | Charity added |
| `news` | `article` | News article |
| `control` | `auth_pong` | Authentication pong |
| `control` | `twitter_handles_result` | Handle 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.

### Replay a recovery window

```typescript
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.

| Status | Likely cause | Fix |
| --- | --- | --- |
| `400` | Missing or invalid WebSocket protocol headers | Send `tweetstream.v1` and a valid auth token protocol |
| `401` | Missing, empty, or invalid API key | Regenerate or rotate the API key from the dashboard |
| `403` | Subscription state does not allow live streaming | Start a trial, renew, or upgrade the account |
| `429` | Active WebSocket connection limit reached | Close an old socket or move to a higher plan |
| `503` | Connection limit service temporarily unavailable | Retry with backoff |

### Read an upgrade denial

```json
{
  "error": "WebSocket connection limit reached (3/3 active). Close an existing connection and retry.",
  "status": 429
}
```
