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. 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 |
Authenticate a server connection
typescriptconst 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.
Node.js reconnecting consumer
typescriptimport 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.
| 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 |
| Handle requests | 429, or the limit error in twitter_handles_result | Batch changed handles, up to 100 per request, then back off before retrying |