Guias/WebSocket de eventos
Documentação

WebSocket de eventos

Assinatura em tempo real de eventos por instância e eventos globais de Message Batch.

O WebSocket entrega, em tempo real e em modo best-effort, o mesmo JSON serializado para os webhooks. Headers HTTP de webhook nao sao enviados pelo WebSocket.

URLs

Local:

ws://localhost:8084/ws/instance/events?event=connection.update&token=<INSTANCE_JWT>
ws://localhost:8084/ws/global/events?event=message.batch.progress&token=<USER_JWT>

Producao deve usar wss://:

wss://api.example.com/ws/instance/events
wss://api.example.com/ws/global/events

Cada conexao assina exatamente um evento por query string. Nao ha wildcard, *, multiplos eventos separados por virgula ou troca de assinatura depois do upgrade.

Autenticacao

/ws/instance/events aceita somente JWT de instancia. O token e validado com HS256 e AUTHENTICATION_JWT_SECRET, precisa conter instanceName, precisa pertencer ao registro Auth.token da instancia encontrada e entrega apenas eventos desse Instance.id.

Quando AUTHENTICATION_JWT_EXPIRES_IN > 0, a expiracao do token de instancia e validada no handshake e a conexao e fechada no instante de exp. Quando AUTHENTICATION_JWT_EXPIRES_IN=0, a expiracao do token de instancia pode ser ignorada e nao ha timer artificial.

/ws/global/events aceita somente JWT de usuario assinado com o mesmo segredo HS256. O claim userId em formato UUID e obrigatorio e identifica o dono dos eventos de lote. O claim exp e sempre obrigatorio e sempre validado, mesmo quando AUTHENTICATION_JWT_EXPIRES_IN=0.

O runtime atual nao possui tabela de usuarios. Por isso, a validacao do usuario no WebSocket e criptografica e baseada em userId; a associacao do lote e feita por ownerUserId, persistido a partir do mesmo claim na criacao do lote.

Parametros

ParametroObrigatorioDescricao
eventSimNome externo exato do evento. A comparacao e case-sensitive.
tokenSimJWT de instancia ou de usuario, conforme o endpoint.

Respostas antes do upgrade:

StatusCaso
400event ou token ausente, evento malformado ou evento fora da allowlist.
401Token invalido, expirado ou assinatura incorreta.
403Token valido, mas do escopo errado, sem permissao, ou origem recusada.
404Instancia referenciada pelo token de instancia nao encontrada.
426Rota chamada sem upgrade WebSocket.
500Falha interna inesperada.

Eventos de instancia

FlagEvento
callUpsertcall.upsert
chatsDeletedchats.delete
chatsUpdatedchats.updated
connectionUpdatedconnection.update
contactsUpdatedcontacts.update
contactsUpsertcontacts.upsert
groupsParticipantsUpdatedgroups.participants.update
groupsUpdatedgroups.update
groupsUpsertgroups.upsert
historySynchistory.sync
identityUpdatedidentity.update
labelsAssociationlabels.association
labelsEditlabels.edit
mediaRetrymedia.retry
messagesDeletedmessages.delete
messagesStarredmessages.star
messagesUndecryptablemessages.undecryptable
messagesUpdatedmessages.update
messagesUpsertmessages.upsert
newsLetternews.letter
presenceUpdatedpresence.updated
profilePictureUpdatedprofile.picture.update
qrcodeUpdatedqrcode.updated
sendMessagesend.message
settingsUpdatedsettings.update
statusInstancestatus.instance
userAboutUpdateduser.about.update

messaging-history.set nao e disponibilizado. history.sync e aceito normalmente.

Eventos globais

Evento
message.batch.created
message.batch.started
message.batch.progress
message.batch.pause-requested
message.batch.paused
message.batch.waiting-instance
message.batch.resumed
message.batch.stop-requested
message.batch.stopped
message.batch.interrupted
message.batch.completed
message.batch.completed-with-errors
message.batch.item.failed
message.batch.item.unknown

Eventos globais sao isolados por ownerUserId == userId do JWT de usuario.

Payload

Eventos de instancia usam exatamente o envelope de internal/webhook.WebhookPayload:

{
  "event": "connection.update",
  "instance": {"id": 1, "name": "minha-instancia", "connectionStatus": "online", "ownerJid": "[email protected]", "externalAttributes": {}},
  "data": {"type": "connected", "connection": "open"},
  "timestamp": "2026-07-14T12:00:00Z"
}

messages.upsert:

{
  "event": "messages.upsert",
  "instance": {"id": 1, "name": "minha-instancia", "connectionStatus": "online", "ownerJid": "[email protected]", "externalAttributes": {}},
  "data": {
    "id": 1001,
    "keyRemoteJid": "[email protected]",
    "keyLid": null,
    "keyFromMe": false,
    "keyParticipant": null,
    "keyParticipantLid": null,
    "pushName": "Contato",
    "messageType": "text",
    "content": {"text": "Ola"},
    "messageTimestamp": 1784040000,
    "device": "web",
    "isGroup": false,
    "metadata": {}
  },
  "timestamp": "2026-07-14T12:00:00Z"
}

qrcode.updated:

{
  "event": "qrcode.updated",
  "instance": {"id": 1, "name": "minha-instancia", "connectionStatus": "qr_code", "ownerJid": null, "externalAttributes": {}},
  "data": {"count": 1, "code": "2@codigo-ficticio", "base64": "data:image/png;base64,iVBORw0KGgoAAA...", "expiresInSeconds": 30, "expiresAt": "2026-07-14T12:00:30Z"},
  "timestamp": "2026-07-14T12:00:00Z"
}

Eventos globais usam o envelope de lote:

{
  "event": "message.batch.progress",
  "timestamp": "2026-07-14T12:00:00Z",
  "data": {
    "batch": {
      "id": "019f6086-ce48-7ba3-83ca-02c15405d570",
      "ownerUserId": "11111111-1111-4111-8111-111111111111",
      "name": "campanha",
      "status": "PROCESSING",
      "instances": [{"id": 1, "name": "minha-instancia", "connectionStatus": "online"}],
      "counts": {"total": 10, "processed": 5, "pending": 5, "sending": 0, "success": 5, "failed": 0, "skipped": 0, "unknown": 0},
      "progress": 50,
      "createdAt": "2026-07-14T11:59:00Z",
      "updatedAt": "2026-07-14T12:00:00Z",
      "currentItem": {"id": "019f6087-0000-7000-8000-000000000001", "position": 4, "recipient": "5511888888888", "instanceName": "minha-instancia"}
    }
  }
}

message.batch.completed:

{
  "event": "message.batch.completed",
  "timestamp": "2026-07-14T12:05:00Z",
  "data": {
    "batch": {
      "id": "019f6086-ce48-7ba3-83ca-02c15405d570",
      "ownerUserId": "11111111-1111-4111-8111-111111111111",
      "name": "campanha",
      "status": "COMPLETED",
      "instances": [{"id": 1, "name": "minha-instancia", "connectionStatus": "online"}],
      "counts": {"total": 10, "processed": 10, "pending": 0, "sending": 0, "success": 10, "failed": 0, "skipped": 0, "unknown": 0},
      "progress": 100,
      "completedAt": "2026-07-14T12:05:00Z",
      "createdAt": "2026-07-14T11:59:00Z",
      "updatedAt": "2026-07-14T12:05:00Z"
    }
  }
}

message.batch.item.failed:

{
  "event": "message.batch.item.failed",
  "timestamp": "2026-07-14T12:03:00Z",
  "data": {
    "batchId": "019f6086-ce48-7ba3-83ca-02c15405d570",
    "itemId": "019f6087-0000-7000-8000-000000000009",
    "recipient": "5511777777777",
    "error": {"code": "SEND_MESSAGE_FAILED", "message": "Falha ao enviar a mensagem."},
    "instance": {"id": 1, "name": "minha-instancia"}
  }
}

Cliente JavaScript

function createSocketClient({ url, token, reconnectInterval = 5000 }) {
  return function socket(eventName, callback) {
    let ws = null;
    let reconnectTimer = null;
    let stopped = false;

    function connect() {
      const query = new URLSearchParams({ event: eventName, token });
      ws = new WebSocket(`${url}?${query.toString()}`);
      ws.onmessage = (messageEvent) => callback?.(JSON.parse(messageEvent.data), messageEvent);
      ws.onclose = () => {
        if (!stopped) reconnectTimer = setTimeout(connect, reconnectInterval);
      };
    }

    connect();

    return {
      close(code = 1000, reason = "Client closed connection") {
        stopped = true;
        if (reconnectTimer) clearTimeout(reconnectTimer);
        if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) ws.close(code, reason);
      },
      get connection() {
        return ws;
      }
    };
  };
}

const instanceSocket = createSocketClient({url: "ws://localhost:8084/ws/instance/events", token: "jwt-da-instancia"});
const connectionSubscription = instanceSocket("connection.update", (data) => console.log("connection.update", data));
const messagesSubscription = instanceSocket("messages.upsert", (data) => console.log("messages.upsert", data));
const qrcodeSubscription = instanceSocket("qrcode.updated", (data) => console.log("qrcode.updated", data));

const globalSocket = createSocketClient({url: "ws://localhost:8084/ws/global/events", token: "jwt-do-usuario"});
const progressSubscription = globalSocket("message.batch.progress", (data) => console.log("message.batch.progress", data));
const completedSubscription = globalSocket("message.batch.completed", (data) => console.log("message.batch.completed", data));
const failedItemSubscription = globalSocket("message.batch.item.failed", (data) => console.log("message.batch.item.failed", data));

Heartbeat e fechamento

O servidor envia ping periodico, atualiza o deadline ao receber pong e aplica write deadline. O browser responde aos frames ping automaticamente.

CodigoSignificado
1000Encerramento normal.
1001Servidor encerrando ou conexao removida.
1008Mensagem de aplicacao enviada pelo cliente ou violacao de politica.
1011Erro interno inesperado.
4001Token expirou durante a conexao.
4003Permissao revogada ou escopo nao permitido.
4008Cliente lento ou buffer de envio excedido.

Configuracao

VariavelPadraoDescricao
WEBSOCKET_ENABLEDtrueRegistra ou desabilita as rotas /ws/....
WEBSOCKET_ALLOWED_ORIGINSvazioLista separada por virgula de origens aceitas. Use origens explicitas em producao.
WEBSOCKET_ALLOW_EMPTY_ORIGINtruePermite clientes backend sem header Origin.
WEBSOCKET_PING_INTERVAL25sIntervalo de ping do servidor.
WEBSOCKET_PONG_TIMEOUT60sPrazo para receber pong.
WEBSOCKET_WRITE_TIMEOUT10sPrazo de escrita por frame.
WEBSOCKET_SEND_BUFFER256Buffer por cliente. Cliente lento e removido quando o buffer enche.

Seguranca e troubleshooting

O token vai na query string porque WebSocket nativo no browser nao permite header Authorization customizado. A API nao registra query string nas rotas WebSocket, nao devolve JWT no payload e nao inclui tokens em logs. Configure proxy e observabilidade para nao registrar a query completa.

Nao ha replay automatico. Se o cliente estiver desconectado, eventos podem ser perdidos; use os endpoints REST para reconstruir estado persistente.

SintomaCausa provavel
426A chamada nao fez upgrade WebSocket.
400Evento ausente, errado, com caixa incorreta ou fora da allowlist.
401JWT invalido, expirado ou sem exp no caso de usuario.
403Token de outro escopo, origem recusada ou token de instancia diferente do Auth.token.
Fecha com 4001O token expirou depois da conexao estabelecida.