Pular para o conteúdo principal

WebSocket

Além de webhooks (POST para uma URL sua), a API expõe os mesmos eventos em tempo real por WebSocket — útil quando você quer consumir os eventos diretamente no seu processo, sem manter um endpoint HTTP público. É o mesmo canal que o portal (panel/) usa para mostrar o QR code e o status da instância ao vivo, sem dar poll.

Conectando

GET /ws/:instance

:instance é o nome da instância (o mesmo usado nas rotas REST). A conexão é um WebSocket puro (@fastify/websocket) — sem sub-protocolo especial, sem handshake customizado além do upgrade padrão HTTP → WS.

Credencial vai na querystring, não no header

Esta é a parte que difere do resto da API: em toda outra rota, a credencial (apikey ou Authorization: Bearer <jwt>) vai em um header HTTP. No handshake de WebSocket isso não é possível — o navegador não permite enviar headers customizados ao abrir um WebSocket. Por isso, só para rotas /ws/*, a API aceita a credencial também via querystring (src/server.ts, função resolveCredential):

GET /ws/minha-instancia?token=<JWT>
GET /ws/minha-instancia?apikey=<SUA_API_KEY>
  • ?token=<JWT> — o mesmo token de sessão devolvido por POST /auth/login (é o que o portal usa; veja panel/src/lib/ws.ts).
  • ?apikey=<SUA_API_KEY> — a API key global (AUTHENTICATION_API_KEY), para conectar direto de um backend/servidor sem passar por login.

Qualquer uma das duas basta. Fora das rotas /ws/*, a API não aceita credencial via querystring — só header — então não tente reaproveitar esse padrão em outras chamadas.

Autenticando por apikey numa instância com token próprio: mesmo parâmetro token, papel diferente

Se a instância tiver um token próprio configurado (endurecimento opcional, ver POST /instance/create) e você estiver conectando com ?apikey=, é preciso também passar ?token=<token-da-instância> — literalmente o mesmo nome de parâmetro token, só que aqui comparado contra Instance.token em vez de decodificado como JWT (src/server.ts, hook preHandler). Quando a conexão já é autenticada por JWT (?token=<JWT> que passou na verificação), esse reforço é dispensado — o controle de acesso nesse caso já é o papel do usuário do portal. Ou seja: ?token= é JWT ou token da instância, dependendo de qual credencial autenticou a conexão — nunca os dois ao mesmo tempo pelo mesmo parâmetro.

O que chega pelo socket

Cada mensagem recebida no socket é um frame de texto com o mesmo JSON que seria enviado por webhook — o mesmo envelope { instance, event, timestamp, data }, com o mesmo formato de data por evento. Veja Webhooks para a tabela completa e um exemplo de payload de cada evento — não duplicado aqui de propósito, para não haver duas fontes divergentes.

Duas diferenças reais em relação a webhooks, ambas em src/ws/index.ts e src/schedulers/index.ts:

  • O WebSocket não filtra por webhookEvents — a instância assinada recebe todos os eventos que acontecerem nela pelo socket, independente da lista configurada no PATCH /instance/:name/webhook (esse filtro só vale para a entrega HTTP; broadcast() é chamado antes do filtro de assinatura). Se você só quer um subconjunto de eventos, filtre no seu próprio código ao receber a mensagem.
  • instance.health não chega pelo WebSocket. Dos 8 eventos, só os 7 que passam pelo EVENT_MAP (message.received, message.sent, message.ack, qrcode.updated, connection.update, group.participants.update, call.received) são transmitidos ao vivo no socket. O scheduler de saúde que dispara instance.health (kill switch, a cada 5 minutos) só chama enqueueWebhook — nunca broadcast. Se seu sistema precisa reagir à instância entrando em vermelho, hoje isso só chega por webhook (com webhookUrl própria configurada na instância, ver Webhooks), não por este socket.

Não há reconexão automática embutida no servidor — se a conexão cair (restart do servidor, queda de rede, etc.), o cliente precisa abrir um novo WebSocket para o mesmo /ws/:instance.

Exemplos

// O browser nao envia headers no handshake do WebSocket — por isso o
// token vai na querystring. Mesmo padrao usado pelo portal
// (panel/src/lib/ws.ts).
const instancia = 'minha-instancia';
const token = localStorage.getItem('token'); // JWT de POST /auth/login
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
const url = `${proto}//${location.host}/ws/${encodeURIComponent(instancia)}?token=${encodeURIComponent(token)}`;

const ws = new WebSocket(url);

ws.onmessage = (msg) => {
const evento = JSON.parse(msg.data);
console.log(evento.event, evento.data);
if (evento.event === 'qrcode.updated') {
// evento.data.qr e a string bruta do QR — renderize como PNG no seu app
}
if (evento.event === 'connection.update') {
// evento.data.status: 'connected' | 'disconnected' | ...
}
// 'instance.health' (kill switch) NAO chega por este socket — so por
// webhook, ver /webhooks#instancehealth.
};

ws.onerror = (e) => console.error('WebSocket com erro', e);
ws.onclose = () => console.warn('WebSocket fechado — reconecte se necessário');
Eventos ao vivo durante um pareamento real ainda não foram observados em produção

O que está descrito nesta página é o que o código faz (src/ws/index.ts, panel/src/lib/ws.ts) — mas o fluxo completo de eventos chegando ao vivo durante um pareamento real por QR Code, com um Chromium headless de verdade dentro do container, ainda não foi validado ponta a ponta em produção neste projeto (mesma ressalva já feita em Primeiros passos). Se a conexão cair sem aviso ou os eventos não chegarem na ordem esperada, isso pode ser o primeiro sinal real desse fluxo, não necessariamente um bug no seu cliente.