Transmisión
wss wss://ws.xsignalfeed.com/v1/stream
Conexión
El stream acepta tu clave de las mismas tres formas que REST. Desde un servidor usa una cabecera; usa el parámetro de URL solo donde no se puedan poner cabeceras. Si la clave no llega o no se acepta, la conexión se abre y se cierra enseguida con 4001, y el motivo del cierre dice por qué.
| Cómo enviar la clave | Úsalo cuando |
|---|---|
Authorization: Bearer xsf_… | Recomendado para servidores; la misma cabecera que en REST |
X-API-Key: xsf_… | Equivale a la anterior |
?api_key=xsf_… | Navegadores y clientes que no pueden poner cabeceras en el handshake. La clave pasa a formar parte de la URL, así que mantén esas URL fuera de los logs |
python — clave en una cabecera
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", ...}
| Parámetro de consulta | Significado |
|---|---|
| api_key | Tu clave, cuando no puedes fijar la cabecera |
| last_id | Reanudar después de este sid. Se ignora en silencio a menos que la clave tenga el ámbito replay |
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) }
Trama
Tres tramas son nuestras y no de X:
| Trama | Cuándo |
|---|---|
system.hello | Primera trama. targets es cuántas cuentas vigila esta clave en este momento (las cuentas añadidas después se incorporan sin reconectar); replay_from devuelve el last_id con el que reconectaste y es null si no enviaste ninguno, en cuyo caso no se reproduce nada |
system.ping | Se envía durante los silencios como señal de vida, para que los proxies inactivos no cierren el socket |
system.error | Algo falla en la conexión; le sigue un cierre |
Código de cierre
El handshake siempre tiene éxito y el cierre llega justo después, para que tu biblioteca cliente vea el código y el motivo en lugar de un error HTTP a secas.
| Código de cierre | Significado |
|---|---|
1012 | Reinicio del servicio durante un mantenimiento. Reconecta con espera exponencial y pasa el último sid que procesaste como ?last_id=: los eventos del hueco se reproducen y no se pierde nada |
4001 | La clave no se aceptó. El motivo del cierre dice por qué: missing_api_key (no nos llegó ninguna clave: revisa el nombre de la cabecera, o que tu proxy o librería cliente la reenvíe), invalid_api_key (no existe esa clave: un error tipográfico o un secreto sustituido por una rotación), key_revoked (la clave fue revocada) |
4002 | Demasiadas conexiones concurrentes para esta clave; una trama system.error precede al cierre |
4003 | La clave carece del ámbito stream |
Reconectar correctamente
Escribe esto una vez, y no la versión ingenua. Las dos cosas que importan son el retroceso exponencial y persistir sid en algún sitio que sobreviva a un reinicio del proceso.
python — forma de producción
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)