PEPINO ZAPdocs
Webhooks

WebSocket (tempo real)

Receba os eventos do PEPINO ZAP por uma conexão WebSocket, sem precisar expor um endpoint público — endpoint wss, autenticação por ?token=, envelope dos eventos, ciclo de vida e tratamento de erros.

Ver como Markdown

O WebSocket é um transporte alternativo ao webhook para receber eventos em tempo real. Em vez de a plataforma chamar o seu endpoint (como faz o webhook), é o seu cliente que abre uma conexão persistente com o PEPINO ZAP e passa a receber os eventos enquanto estiver conectado.

É a opção indicada quando você não tem um endpoint público para receber POST — por exemplo, um frontend, uma SPA, um dashboard interno ou um protótipo rodando na sua máquina. Como a conexão parte do seu lado, não há nada para expor à internet nem firewall para configurar.

Direto do navegador, só em contexto de confiança. A autenticação é ?token=<API_KEY> (ver Autenticação) — então qualquer pessoa com acesso àquela página vê a key (aba de rede / JS) e ganha acesso total à API. Isso é aceitável para um dashboard interno, uma ferramenta sua ou um protótipo (você é o dono da key), mas não para um app público / multi-usuário. Nesse caso, mantenha a key no servidor: receba os eventos por webhook e repasse ao browser pelo seu próprio canal, ou — sem manter socket de pé — o browser faz polling no seu servidor, que já tem os dados do webhook. E para um app multi-usuário que ainda quer push direto no navegador, use o token efêmero: o seu backend troca a key por um token curto e escopado, e o navegador usa ele — a ?token= nunca é a key crua.

Os eventos entregues são os mesmos do webhook — os dez tipos de Eventos (message.received, message.status, instance.status, call.received, message.deleted, group.participants, group.updated, contact.updated, label.updated, label.association) — com o mesmo envelope JSON. Além deles, o WebSocket entrega presence.update (online/digitando), exclusivo deste transporte (ver abaixo). Você não precisa aprender outro formato: o que você já sabe da página de Eventos vale aqui sem mudança.

O WebSocket não substitui o webhook quando você precisa de garantia de entrega. Ele entrega o evento apenas enquanto o cliente está conectado — não há buffer nem retry. Se a conexão estiver caída no instante do evento, esse evento não é reentregue depois. Para entrega durável (com reenvio em caso de falha), use webhooks. Os dois transportes podem rodar ao mesmo tempo.

Endpoint

A conexão é feita sobre o protocolo seguro wss:// (WebSocket sobre TLS), no caminho /v1/ws:

ModoURL
Produçãowss://api.pepinozap.com.br/v1/ws
Teste (sandbox)wss://sandbox.pepinozap.com.br/v1/ws

O host decide o modo (produção ou teste), e a sua API key precisa casar com o modo do host — ver Autenticação abaixo. Use sempre wss:// (criptografado); conexões ws:// em texto puro não são aceitas.

Uma conexão é escopada ao Projeto da API key e recebe os eventos de todas as instâncias desse Projeto — você não filtra por instância na URL. Para distinguir de qual instância veio cada evento, use o campo instanceId do envelope (ver Envelope dos eventos).

Autenticação

O handshake de um WebSocket no navegador não permite enviar o header Authorization (a API WebSocket não aceita headers customizados). Por isso, em vez do header Authorization: Bearer <API_KEY> usado nos demais endpoints, a API key é passada no parâmetro de query token:

wss://api.pepinozap.com.br/v1/ws?token=pzk_live_xxxxxxxxxxxxxxxx

Use a mesma API key que você usaria no header Authorization. A key precisa ser coerente com o modo do host:

HostModoPrefixo de key exigido
api.pepinozap.com.brProduçãopzk_live_
sandbox.pepinozap.com.brTestepzk_test_

Conectar com uma key do modo errado (por exemplo, pzk_test_ no host de produção) é rejeitado com 401. O modo é resolvido pelo host; a key apenas tem que combinar.

Como a key vai na URL, evite registrá-la em logs de acesso, no histórico do navegador ou no header Referer. Para testes, prefira sempre o ambiente de sandbox com uma key pzk_test_. Se uma key de produção for exposta dessa forma, revogue-a e gere outra no painel.

O que pode falhar no handshake

A autenticação acontece durante o handshake HTTP, antes do upgrade para WebSocket. Quando ela falha, o servidor responde com um status HTTP normal (a conexão WebSocket nem chega a abrir) e o corpo segue o envelope de erro padrão da API, { "error": { "code", "message" } }:

StatuscodeQuando acontece
401UNAUTHORIZED?token= ausente; key inválida, revogada ou expirada; ou key do modo errado para o host (pzk_live_ no sandbox, ou vice-versa).
403FORBIDDENA key é válida, mas o Projeto não está ativo.
426A ?token= foi aceita, mas a requisição chegou em /v1/ws sem cabeçalhos de upgrade de WebSocket (por exemplo, um GET comum, com a key na query, mas sem fazer o upgrade). Use um cliente WebSocket de verdade.

No navegador, a API WebSocket não expõe o status nem o corpo da resposta de falha do handshake — você recebe apenas um evento onclose/onerror genérico, sem o motivo. Se a conexão fechar logo após abrir, valide o ?token= e o host com uma ferramenta que mostre o corpo HTTP (por exemplo, curl apontando para o endpoint via http(s):// no lugar de wss://, ou um cliente WebSocket de linha de comando). Em produção, faça um pré-teste do par host + key em sandbox antes de subir.

Token efêmero para o navegador

Passar a ?token=<API_KEY> direto no navegador expõe a key — tudo bem num dashboard interno, mas não num app público/multi-usuário, onde qualquer pessoa na página pegaria a key e teria acesso total à API. Para esse caso, o seu backend troca a key por um token efêmero e entrega só ele ao navegador.

1. No seu servidor (com a API key, que nunca vai ao browser), peça um token:

POST /v1/ws/token
Authorization: Bearer pzk_live_xxxxxxxxxxxxxxxx

Resposta:

{ "token": "eyJhbGciOi...", "tokenType": "ws", "expiresIn": 900 }

Esse token vale ~15 min (expiresIn, em segundos), é escopado ao Projeto da key e só serve para abrir este WebSocket — não autentica nenhum outro endpoint. Vazou? No máximo abre um WS de leitura daquele Projeto, e expira sozinho.

2. No navegador, conecte com o token efêmero no lugar da key:

wss://api.pepinozap.com.br/v1/ws?token=eyJhbGciOi...

3. Reconexão sem queda. O token é checado só no handshake — a conexão já aberta não cai quando ele expira. Se o socket cair (rede) e o token já tiver expirado, peça um /v1/ws/token novo ao seu backend antes de reconectar (1 chamada, transparente para o usuário final).

É o mesmo princípio do painel da PEPINO ZAP: o navegador nunca carrega uma credencial de acesso total — só um token curto, escopado e renovável; a API key crua fica sempre no servidor.

Filtrar por evento

Por padrão, a conexão recebe todos os eventos do Projeto. Para receber apenas alguns, passe o parâmetro events na URL, com os nomes separados por vírgula — os mesmos nomes usados no webhook (ver Eventos). Sem o parâmetro (ou com a lista vazia), você recebe todos.

wss://api.pepinozap.com.br/v1/ws?token=pzk_live_xxxx&events=message.received,call.received
events na URLA conexão recebe
(ausente)Todos os eventos
message.receivedmessage.received
message.received,message.statusOs dois listados

O filtro vale só para esta conexão (é por conexão, não por Projeto) — abra conexões diferentes, com filtros diferentes, se precisar separar fluxos. Nomes desconhecidos são ignorados (não casam com nada). O frame inicial connected é sempre enviado, independente do filtro.

Envelope dos eventos

Cada evento chega como um frame de texto com um JSON idêntico ao corpo entregue pelo webhook — mesmo formato, mesmos campos:

{
  "event": "message.received",
  "instanceId": "cm5x8a9k20001abc123def456",
  "timestamp": "2026-05-30T12:00:00Z",
  "data": {}
}

Campos do envelope:

  • event — nome do evento (um dos dez tipos de webhook — message.received, message.status, instance.status, call.received, message.deleted, group.participants, group.updated, contact.updated, label.updated, label.association — ou presence.update, exclusivo do WebSocket; ver Eventos).
  • instanceId — identificador da instância que originou o evento (use para saber de qual número veio, já que uma conexão recebe todas as instâncias do Projeto).
  • timestamp — data e hora do evento em ISO 8601 (UTC).
  • data — objeto com os dados específicos do evento.

O conteúdo de data varia conforme o event. Os três eventos e o payload completo de cada um (incluindo os formatos de from, os valores de status e o campo mediaUrl) estão documentados em Eventos — são os mesmos aqui.

Frame de confirmação inicial (connected)

Logo após o handshake, antes de qualquer evento, o servidor envia um frame de confirmação informando o Projeto ao qual a conexão foi escopada:

{ "event": "connected", "projectId": "proj_123" }

Esse é o sinal de que a conexão está autenticada e pronta para receber eventos. Trate-o como um caso à parte no seu onmessage: ele não tem o envelope de evento (instanceId, timestamp, data), então não tente processá-lo como uma mensagem. Receber o connected é a confirmação de que o ?token= foi aceito.

Frame de erro (error)

Em uma falha de autorização detectada após o upgrade, o servidor pode enviar um frame de erro e encerrar a conexão:

{ "event": "error", "message": "unauthorized" }

Na prática, as falhas de credencial são barradas ainda no handshake (ver O que pode falhar no handshake); este frame é a salvaguarda para o caso de o contexto de autorização não chegar ao handler. Se você receber um frame com event: "error", não reconecte em loop sem antes revalidar o ?token= — provavelmente a credencial está incorreta.

Como diferenciar os frames no seu onmessage: faça JSON.parse e olhe o campo event. Se for connected, é a confirmação inicial (ignore). Se for error, é uma falha (revise a credencial). Qualquer outro valor (message.received, message.status, instance.status) é um evento real para processar.

Ciclo de vida da conexão

Entender o ciclo de vida evita os dois erros mais comuns: achar que precisa "fazer keepalive" manualmente e não tratar a reconexão.

  • Ping / keepalive (servidor → cliente). O servidor envia um frame de controle ping a cada 25 segundos para manter a conexão viva e detectar quedas. Navegadores e a grande maioria das bibliotecas WebSocket respondem o pong automaticamente — você não precisa fazer nada. Esse ping é um frame de controle do protocolo WebSocket, não um frame de texto: ele não dispara o seu onmessage, então você nunca vai ver um evento "ping" no payload (diferente do stream SSE de QR, que emite um evento ping de texto).
  • Canal de saída apenas (servidor → cliente). O cliente não precisa enviar nenhuma mensagem — o canal é unidirecional na prática. O servidor lê os frames que você porventura enviar só para detectar o fechamento da conexão; nada que você mande é interpretado como comando.
  • Reconexão. Se a conexão cair (queda de rede, deploy, timeout de proxy), reconecte e continue recebendo. Como não há buffer, qualquer evento ocorrido durante o intervalo desconectado é perdido — por isso a reconexão deve ser rápida. Recomenda-se um backoff exponencial (por exemplo, 1s, 2s, 4s, 8s… até um teto de ~30s) para não martelar o servidor numa indisponibilidade.

Proxies e balanceadores podem encerrar conexões ociosas. O ping de 25s do servidor normalmente mantém a conexão viva nesses intermediários, mas se você observar quedas periódicas atrás de um proxy corporativo, a causa costuma ser um timeout de idle do proxy — a reconexão com backoff resolve do lado do cliente. Para um cenário onde nenhum evento pode ser perdido, o transporte recomendado é o webhook (com retry).

Exemplo completo (JavaScript)

O exemplo abaixo cobre o caso real: confirma o connected, processa os eventos por tipo, trata o frame de error e reconecta com backoff exponencial.

const TOKEN = 'pzk_live_xxxxxxxxxxxxxxxx';
const URL = `wss://api.pepinozap.com.br/v1/ws?token=${TOKEN}`;

let backoff = 1000; // 1s inicial
const MAX_BACKOFF = 30000; // teto de 30s

function connect() {
  const ws = new WebSocket(URL);

  ws.onopen = () => {
    // Handshake aceito. O servidor enviará o frame "connected" em seguida.
  };

  ws.onmessage = (e) => {
    const evt = JSON.parse(e.data);

    if (evt.event === 'connected') {
      backoff = 1000; // conexão saudável: zera o backoff
      return;
    }
    if (evt.event === 'error') {
      // Falha de autorização: revise o ?token= antes de reconectar.
      console.error('WS error:', evt.message);
      return;
    }

    // Evento real (mesmo envelope do webhook).
    switch (evt.event) {
      case 'message.received':
        console.log('Mensagem de', evt.instanceId, evt.data);
        break;
      case 'message.status':
        console.log('Status de entrega', evt.data.status, evt.data);
        break;
      case 'instance.status':
        console.log('Conexão da instância', evt.data.status);
        break;
    }
  };

  ws.onclose = () => {
    // Reconecta com backoff exponencial.
    setTimeout(connect, backoff);
    backoff = Math.min(backoff * 2, MAX_BACKOFF);
  };

  ws.onerror = () => {
    ws.close(); // força o onclose e o fluxo de reconexão
  };
}

connect();

No modo teste, troque a URL para wss://sandbox.pepinozap.com.br/v1/ws e use uma key pzk_test_.

Presença ao vivo — presence.update (exclusivo do WebSocket)

Indicadores de presença — "digitando…", "gravando áudio…", parou de digitar e (online/offline) — são um sinal de altíssima frequência e efêmero. Por isso, ao contrário dos demais eventos, o presence.update é entregue apenas pelo WebSocket, ao vivo: não vai por webhook e não é persistido. Se ninguém está conectado ao WS naquele instante, ele é descartado, sem custo. É o que mantém a plataforma leve e evita inundar a fila de webhooks com ruído.

{
  "event": "presence.update",
  "instanceId": "inst_123",
  "timestamp": "2026-06-06T12:00:00Z",
  "data": {
    "from": "5511999999999@s.whatsapp.net",
    "presence": "typing"
  }
}
CampoTipoDescrição
fromstringO contato/chat de onde vem a presença.
presencestringtyping (digitando), recording (gravando áudio), paused (parou de digitar), online ou offline.
lastSeenstringSó em offline, quando disponível: o "visto por último" em ISO 8601 (UTC). Ausente se o contato oculta o "visto por último".

Os indicadores de digitação/gravação chegam naturalmente para conversas ativas. Já online/offline o WhatsApp só envia para contatos que você assina explicitamente — a plataforma não assina presença em massa por padrão (justamente para se manter leve), então, na prática, você verá sobretudo typing / recording / paused. Para receber presença, conecte com ?events=presence.update (ver Filtrar por evento).

WebSocket ou webhook?

As duas opções entregam os mesmos eventos, com o mesmo envelope. A escolha depende de dois fatores: se você tem um endpoint público e qual garantia de entrega você precisa.

WebSocketWebhook
Endpoint públicoNão precisa (o cliente conecta)Necessário (a plataforma chama você)
Sentido da conexãoCliente → plataformaPlataforma → seu endpoint
Autenticação?token= na URLHeader + assinatura X-Pepino-Signature
EntregaSó enquanto conectado — sem buffer, sem retryCom retry e backoff, até 6 tentativas
Eventos perdidos na quedaPerdidos (não há reentrega)Reenviados até confirmação 2xx
Indicado paraFrontends, dashboards, protótiposIntegrações de backend que exigem garantia

Regra prática: use WebSocket quando o consumidor é um frontend ou ferramenta sem URL pública e a perda eventual de um evento é tolerável; use webhook quando você precisa que todo evento chegue, mesmo após uma queda. Nada impede usar os dois ao mesmo tempo — por exemplo, webhook como fonte durável no backend e WebSocket para atualizar uma tela ao vivo.

Para o comportamento detalhado de entrega e reenvio do webhook (timeout, número de tentativas, idempotência), consulte Entrega e retry. Para validar a origem das entregas de webhook, veja Assinatura.

Próximos passos

  • Eventos — os três eventos e o payload completo de cada data.
  • Webhooks — o transporte com entrega garantida (retry) e como cadastrá-los via API.
  • Entrega e retry — timeout, backoff e idempotência das entregas por webhook.

On this page