Skip to docs content

Connection, authentication, and heartbeat

Use the documented regional endpoint, authenticate during the upgrade, and leave native ping and pong handling enabled.

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

Choose a base URL

SurfaceURLUse
WebSocket (USA)wss://ws-iad.tweetstream.io/wsUse for connections in the USA
WebSocket (global)wss://ws-global.tweetstream.io/wsUse for connections outside the USA
RESThttps://api.tweetstream.ioBase origin for /api/history, /api/me, and account management
Dashboardhttps://tweetstream.io/dashboardAPI key, watchlists, billing, and Discord routing

Authenticate requests

Authenticate realtime clients with WebSocket subprotocols and REST requests with a bearer token. Keep API keys on the server, never in public browser code.

ContextHeader or protocolNotes
WebSockettweetstream.v1 + tweetstream.auth.token.<API_KEY>Preferred realtime authentication
WebSocket fallbackAuthorization: Bearer <API_KEY> or ?apiKey=<API_KEY>Prefer Authorization; use query authentication only when the runtime cannot set headers
RESTAuthorization: Bearer <API_KEY>History accepts a standard key for Pro or Scale. An active base plan with current Ultra can use its standard key for full History, while a current Ultra key can request full History for its selected accounts. Account management and /api/me accept either key
const socket = new WebSocket("wss://ws-global.tweetstream.io/ws", [
  "tweetstream.v1",
  `tweetstream.auth.token.${process.env.TWEETSTREAM_API_KEY}`,
]);
 
socket.addEventListener("message", (event) => {
  const message = JSON.parse(event.data);
  console.log(message.t, message.op, message.d);
});

Connect with Node.js

Use wss://ws-iad.tweetstream.io/ws in the USA and wss://ws-global.tweetstream.io/ws elsewhere. The server responds with tweetstream.v1; it does not echo the authentication-token protocol. The example reconnects with exponential backoff. Short sessions keep increasing the delay; 30 seconds of uptime resets it.

import WebSocket from "ws";
 
type StreamEvent = {
  t?: string;
  op?: string;
  d?: {
    author?: { handle?: string };
    detected?: unknown;
    text?: string;
  };
};
 
const apiKey = process.env.TWEETSTREAM_API_KEY;
if (!apiKey) {
  throw new Error("Missing TWEETSTREAM_API_KEY");
}
 
let retry = 0;
let healthyTimer: ReturnType<typeof setTimeout> | undefined;
let reconnectTimer: ReturnType<typeof setTimeout> | undefined;
 
function scheduleReconnect(reason: string) {
  if (reconnectTimer) return;
 
  const backoffMs = Math.min(30_000, 1_000 * 2 ** retry);
  const jitterMs = Math.floor(Math.random() * 500);
  const delayMs = backoffMs + jitterMs;
  retry += 1;
  console.warn(`Reconnecting in ${delayMs}ms: ${reason}`);
  reconnectTimer = setTimeout(() => {
    reconnectTimer = undefined;
    connect();
  }, delayMs);
}
 
function connect() {
  const ws = new WebSocket("wss://ws-global.tweetstream.io/ws", [
    "tweetstream.v1",
    `tweetstream.auth.token.${apiKey}`,
  ]);
 
  ws.on("open", () => {
    if (reconnectTimer) clearTimeout(reconnectTimer);
    reconnectTimer = undefined;
    healthyTimer = setTimeout(() => {
      retry = 0;
      healthyTimer = undefined;
    }, 30_000);
    console.log("TweetStream connected");
  });
 
  ws.on("message", (raw) => {
    const event = JSON.parse(raw.toString()) as StreamEvent;
 
    if (event.t === "tweet" && event.op === "content") {
      const tweet = event.d;
      console.log(tweet?.author?.handle, tweet?.text);
    }
 
    if (event.t === "tweet" && event.op === "meta") {
      console.log("enrichment", event.d?.detected);
    }
  });
 
  ws.on("close", (code, reason) => {
    if (healthyTimer) clearTimeout(healthyTimer);
    healthyTimer = undefined;
    scheduleReconnect(`close ${code}: ${reason.toString()}`);
  });
 
  ws.on("unexpected-response", (_request, response) => {
    console.error(
      "Connection rejected",
      response.statusCode,
      response.statusMessage,
    );
    response.resume();
    if (response.statusCode === 429 || response.statusCode === 503) {
      scheduleReconnect(`HTTP ${response.statusCode}`);
      return;
    }
    process.exitCode = 1;
  });
 
  ws.on("error", (error) => {
    console.error("WebSocket error", error.message);
  });
}
 
connect();

Connect with Python

Use any WebSocket runtime that can send both subprotocols. Clean closes and transport errors reconnect with exponential backoff. Short sessions keep increasing the delay; 30 seconds of uptime resets it.

import asyncio
import json
import os
import websockets
 
API_KEY = os.environ["TWEETSTREAM_API_KEY"]
URI = "wss://ws-global.tweetstream.io/ws"
PROTOCOLS = ["tweetstream.v1", f"tweetstream.auth.token.{API_KEY}"]
 
async def main():
    loop = asyncio.get_running_loop()
    retry = 0
    while True:
        connected_at = None
        reason = "connection closed"
        try:
            async with websockets.connect(URI, subprotocols=PROTOCOLS) as ws:
                connected_at = loop.time()
                async for raw in ws:
                    event = json.loads(raw)
                    if event["t"] == "tweet" and event["op"] == "content":
                        tweet = event["d"]
                        handle = tweet.get("author", {}).get("handle")
                        print(handle, tweet.get("text"))
        except Exception as error:
            reason = str(error)
 
        if connected_at is not None and loop.time() - connected_at >= 30:
            retry = 0
        wait = min(30, 2 ** retry)
        retry = min(retry + 1, 5)
        print(f"reconnecting in {wait}s after {reason}")
        await asyncio.sleep(wait)
 
asyncio.run(main())

Handle heartbeats and disconnects

TweetStream sends a native WebSocket ping every 30 seconds. Standard Node and Python clients reply with pong automatically. The server closes an unresponsive socket. Reconnect, then use History API to backfill stored content, profile, follow, and affiliate events.

Handle limits and retries

Your plan sets active WebSocket, monitored-account, and History API limits. Handle add and remove requests share one limit across all your API keys and connections. You can send 100 requests in a burst, then 1 per second. A request with more than 100 handles counts once for every 100 handles, rounded up. On 429, pause that workflow and wait for retryAfterSeconds or the Retry-After header when present. Otherwise, use your normal backoff.

SurfaceLimit signalRecommended handling
WebSocket429 during upgradeClose unused sockets or move to a plan with more connections
History APIretryAfterSeconds when rate limitedWait before replaying the next window
Tracked accountsPlan usage from /api/meCheck count and limit before batch add flows
Handle requests429, or the limit error in twitter_handles_resultBatch changed handles, up to 100 per request, then back off before retrying