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 consultaSignificado
api_keyTu clave, cuando no puedes fijar la cabecera
last_idReanudar 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())

Trama

Tres tramas son nuestras y no de X:

TramaCuándo
system.helloPrimera 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.pingSe envía durante los silencios como señal de vida, para que los proxies inactivos no cierren el socket
system.errorAlgo 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 cierreSignificado
1012Reinicio 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
4001La 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)
4002Demasiadas conexiones concurrentes para esta clave; una trama system.error precede al cierre
4003La 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)