Connection, authentication, and heartbeat
Use the documented regional endpoint, authenticate during the upgrade, and leave native ping and pong handling enabled.
Choose a base URL
| Surface | URL | Use |
|---|---|---|
| WebSocket (USA) | wss://ws-iad.tweetstream.io/ws | Use for connections in the USA |
| WebSocket (global) | wss://ws-global.tweetstream.io/ws | Use for connections outside the USA |
| REST | https://api.tweetstream.io | Base origin for /api/history, /api/me, and account management |
| Dashboard | https://tweetstream.io/dashboard | API 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.
| Context | Header or protocol | Notes |
|---|---|---|
| WebSocket | tweetstream.v1 + tweetstream.auth.token.<API_KEY> | Preferred realtime authentication |
| WebSocket fallback | Authorization: Bearer <API_KEY> or ?apiKey=<API_KEY> | Prefer Authorization; use query authentication only when the runtime cannot set headers |
| REST | Authorization: Bearer <API_KEY> | History accepts a standard key for Pro or Scale. With active or trialing Ultra, AFFILIATE accepts the standard or Ultra key that owns the account. Account management and /api/me accept either key |
Authenticate a server connection
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
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.
Connect with Node.js
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 delayMs = Math.min(30_000, 1_000 * 2 ** retry) + Math.floor(Math.random() * 500);
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.
Python reconnecting consumer
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"]
print(tweet.get("author", {}).get("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. On 429, pause that workflow and wait for retryAfterSeconds when present. Otherwise, use your normal backoff.
| Surface | Limit signal | Recommended handling |
|---|---|---|
| WebSocket | 429 during upgrade | Close unused sockets or move to a plan with more connections |
| History API | retryAfterSeconds when rate limited | Wait before replaying the next window |
| Tracked accounts | Plan usage from /api/me | Check count and limit before batch add flows |