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 porPOST /auth/login(é o que o portal usa; vejapanel/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.
apikey numa instância com token próprio: mesmo parâmetro token, papel diferenteSe 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 noPATCH /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.healthnão chega pelo WebSocket. Dos 8 eventos, só os 7 que passam peloEVENT_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 disparainstance.health(kill switch, a cada 5 minutos) só chamaenqueueWebhook— nuncabroadcast. Se seu sistema precisa reagir à instância entrando em vermelho, hoje isso só chega por webhook (comwebhookUrlpró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
- Browser (JS)
- Node
// 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');
// Node 24 (o runtime deste projeto, ver Dockerfile) traz `WebSocket` como
// global nativo — nao precisa da dependencia `ws` para este exemplo.
const instancia = 'minha-instancia';
const apikey = process.env.API_WHATSAPP_KEY; // AUTHENTICATION_API_KEY
const url = `ws://localhost:8080/ws/${encodeURIComponent(instancia)}?apikey=${encodeURIComponent(apikey)}`;
const ws = new WebSocket(url);
ws.addEventListener('message', (msg) => {
const evento = JSON.parse(msg.data.toString());
console.log(evento.event, evento.data);
});
ws.addEventListener('error', (e) => console.error('WebSocket com erro', e));
ws.addEventListener('close', () => console.warn('WebSocket fechado'));
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.