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 chave | Use 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 query | Significado |
|---|---|
| api_key | Sua chave, quando não é possível definir o cabeçalho |
| last_id | Retomar 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())
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
Três frames são nossos, não do X:
| Frame | Quando |
|---|---|
system.hello | Primeiro 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.ping | Enviado durante o silêncio como keepalive, para que proxies ociosos não fechem o socket |
system.error | Algo 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 fechamento | Significado |
|---|---|
1012 | Reiní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 |
4001 | A 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) |
4002 | Conexões simultâneas demais para esta chave; um frame system.error precede o fechamento |
4003 | A 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)