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.
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:
| Modo | URL |
|---|---|
| Produção | wss://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_xxxxxxxxxxxxxxxxUse a mesma API key que você usaria no header Authorization. A key precisa ser coerente com o modo do host:
| Host | Modo | Prefixo de key exigido |
|---|---|---|
api.pepinozap.com.br | Produção | pzk_live_ |
sandbox.pepinozap.com.br | Teste | pzk_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" } }:
| Status | code | Quando acontece |
|---|---|---|
401 | UNAUTHORIZED | ?token= ausente; key inválida, revogada ou expirada; ou key do modo errado para o host (pzk_live_ no sandbox, ou vice-versa). |
403 | FORBIDDEN | A key é válida, mas o Projeto não está ativo. |
426 | — | A ?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_xxxxxxxxxxxxxxxxResposta:
{ "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.receivedevents na URL | A conexão recebe |
|---|---|
| (ausente) | Todos os eventos |
message.received | Só message.received |
message.received,message.status | Os 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— oupresence.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
pinga cada 25 segundos para manter a conexão viva e detectar quedas. Navegadores e a grande maioria das bibliotecas WebSocket respondem opongautomaticamente — 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 seuonmessage, então você nunca vai ver um evento"ping"no payload (diferente do stream SSE de QR, que emite um eventopingde 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"
}
}| Campo | Tipo | Descrição |
|---|---|---|
from | string | O contato/chat de onde vem a presença. |
presence | string | typing (digitando), recording (gravando áudio), paused (parou de digitar), online ou offline. |
lastSeen | string | Só 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 só 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.
| WebSocket | Webhook | |
|---|---|---|
| Endpoint público | Não precisa (o cliente conecta) | Necessário (a plataforma chama você) |
| Sentido da conexão | Cliente → plataforma | Plataforma → seu endpoint |
| Autenticação | ?token= na URL | Header + assinatura X-Pepino-Signature |
| Entrega | Só enquanto conectado — sem buffer, sem retry | Com retry e backoff, até 6 tentativas |
| Eventos perdidos na queda | Perdidos (não há reentrega) | Reenviados até confirmação 2xx |
| Indicado para | Frontends, dashboards, protótipos | Integraçõ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.
Entrega e retry
Como o PEPINO ZAP entrega cada webhook — método, headers, timeout, critério de sucesso/falha, política de retry com backoff e idempotência.
Visão geral do CRM
As ferramentas de gestão do atendimento do PEPINO ZAP — relatórios, etiquetas, justificativas de encerramento, equipe e permissões, e o construtor de chatbot. O que cada recurso é, onde fica no painel e quais endpoints o expõem.