Streaming

wss wss://ws.xsignalfeed.com/v1/stream

Conectando

O stream aceita sua chave das mesmas três formas que o REST. Em um servidor, use um cabeçalho; use o parâmetro de URL só onde não for possível definir cabeçalhos. Se a chave não chegar ou não for aceita, a conexão abre e fecha logo em seguida com 4001, e o motivo do fechamento diz por quê.

Como enviar a chaveUse quando
Authorization: Bearer xsf_…Recomendado para servidores; o mesmo cabeçalho do REST
X-API-Key: xsf_…Equivale ao anterior
?api_key=xsf_…Navegadores e clientes que não conseguem definir cabeçalhos no handshake. A chave passa a fazer parte da URL, então mantenha essas URLs fora dos logs
python — chave em um cabeçalho
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 querySignificado
api_keySua chave, quando não é possível definir o cabeçalho
last_idRetomar após este sid. Ignorado silenciosamente a menos que a chave tenha o escopo 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())

Frame

Três frames são nossos, não do X:

FrameQuando
system.helloPrimeiro frame. targets é quantas contas esta chave monitora neste momento (contas adicionadas depois entram sem reconectar); replay_from devolve o last_id com que você reconectou e é null quando você não enviou nenhum, caso em que nada é reproduzido
system.pingEnviado durante o silêncio como keepalive, para que proxies ociosos não fechem o socket
system.errorAlgo está errado com a conexão; um fechamento vem em seguida

Código de fechamento

O handshake sempre tem sucesso e o fechamento vem logo em seguida, para que sua biblioteca cliente veja o código e o motivo em vez de um erro HTTP puro.

Código de fechamentoSignificado
1012Reinício do serviço durante uma manutenção. Reconecte com backoff e passe o último sid que você processou como ?last_id=: os eventos da interrupção são reproduzidos e nada se perde
4001A chave não foi aceita. O motivo do fechamento diz por quê: missing_api_key (nenhuma chave chegou até nós: confira o nome do cabeçalho, ou se seu proxy ou biblioteca cliente o repassa), invalid_api_key (essa chave não existe: erro de digitação ou segredo substituído por uma rotação), key_revoked (a chave foi revogada)
4002Conexões simultâneas demais para esta chave; um frame system.error precede o fechamento
4003A chave não tem o escopo stream

Reconectando corretamente

Escreva isto uma vez, e não a versão ingênua. As duas coisas que importam são o backoff exponencial e persistir o sid em algum lugar que sobreviva a uma reinicialização do processo.

python — formato de produção
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)