Streaming
wss wss://ws.xsignalfeed.com/v1/stream
Connecting
The stream takes your key in the same three ways as REST. Use a header from a server; use the URL parameter only where headers cannot be set. If the key does not arrive or is not accepted, the connection opens and then closes at once with 4001, and the close reason says why.
| How to send the key | Use it when |
|---|---|
Authorization: Bearer xsf_… | Recommended for servers; the same header as REST |
X-API-Key: xsf_… | Equivalent to the one above |
?api_key=xsf_… | Browsers, and clients that cannot set handshake headers. The key becomes part of the URL, so keep such URLs out of logs |
python — key in a header
import websockets async with websockets.connect("wss://ws.xsignalfeed.com/v1/stream", additional_headers={"Authorization": "Bearer xsf_your_key"}) as ws: print(await ws.recv()) # {"t": "system.hello", ...}
| Query parameter | Meaning |
|---|---|
| api_key | Your key, when you cannot set the header |
| last_id | Resume after this sid. Silently ignored unless the key has the replay scope |
import asyncio, json, websockets async def main(): url = "wss://ws.xsignalfeed.com/v1/stream?api_key=xsf_your_key" async with websockets.connect(url) as ws: async for raw in ws: ev = json.loads(raw) if ev["t"].startswith("system."): continue print(ev["t"], ev["target"]["screen_name"], ev["d"].get("text", "")[:60]) asyncio.run(main())
import WebSocket from "ws"; const ws = new WebSocket("wss://ws.xsignalfeed.com/v1/stream?api_key=xsf_your_key"); ws.on("message", (raw) => { const ev = JSON.parse(raw); if (ev.t.startsWith("system.")) return; console.log(ev.t, ev.target.screen_name, (ev.d.text ?? "").slice(0, 60)); });
c, _, err := websocket.DefaultDialer.Dial("wss://ws.xsignalfeed.com/v1/stream?api_key=xsf_your_key", nil) for { _, raw, err := c.ReadMessage() if err != nil { break } var ev Event json.Unmarshal(raw, &ev) if strings.HasPrefix(ev.T, "system.") { continue } log.Println(ev.T, ev.Target.ScreenName) }
Frame
Three frames are ours rather than X's:
| Frame | When |
|---|---|
system.hello | First frame. targets is how many accounts this key watches at this moment (accounts added later join without a reconnect); replay_from echoes the last_id you reconnected with and is null when you sent none, in which case nothing is replayed |
system.ping | Sent during silence as a keepalive, so idle proxies do not close the socket |
system.error | Something is wrong with the connection; a close follows |
Close code
The handshake always succeeds and the close follows immediately, so your client library sees the code and the reason rather than a bare HTTP error.
| Close code | Meaning |
|---|---|
1012 | Service restart, during maintenance. Reconnect with backoff and pass the last sid you processed as ?last_id=: the events of the gap are replayed, nothing is lost |
4001 | The key was not accepted. The close reason says why: missing_api_key (no key reached us: check the header name, or that your proxy or client library forwards it), invalid_api_key (no such key: a typo, or a secret replaced by a rotation), key_revoked (the key was revoked) |
4002 | Too many concurrent connections for this key; a system.error frame precedes the close |
4003 | The key lacks the stream scope |
Reconnecting properly
Write this once, not the naive version. The two things that matter are exponential backoff and persisting sid somewhere that survives a process restart.
python — production shape
import asyncio, json, pathlib, random, websockets STATE = pathlib.Path("xsf.cursor") # survives restarts; a DB row is better still async def run(key, handle): backoff = 1 while True: last = STATE.read_text().strip() if STATE.exists() else None url = f"wss://ws.xsignalfeed.com/v1/stream?api_key={key}" + (f"&last_id={last}" if last else "") try: async with websockets.connect(url, ping_interval=20, max_size=2**22) as ws: backoff = 1 async for raw in ws: ev = json.loads(raw) if ev["t"].startswith("system."): continue handle(ev) # must be idempotent: dedupe on ev["id"] if ev.get("sid"): STATE.write_text(ev["sid"]) # only after handling except Exception as e: print("disconnected:", e) await asyncio.sleep(backoff + random.random()) # jitter, or every client returns at once backoff = min(60, backoff * 2)