---
title: "Connection, authentication, and heartbeat"
canonical_url: "https://tweetstream.io/docs/connection-auth-heartbeat"
last_updated: 2026-09-24
---

# Connection, authentication, and heartbeat

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

Canonical URL: https://tweetstream.io/docs/connection-auth-heartbeat

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

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

### Node.js reconnecting consumer

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

### Python reconnecting consumer

```python
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 |
