.method.get { background: var(--success); } .method.put { background: #3B82F6; } h4 { margin-top: 1rem; color: var(--text); } ul, ol { margin: 0.5rem 0 1rem; padding-left: 1.5rem; color: var(--text-muted); } li { margin-bottom: 0.35rem; } .toc { columns: 2; column-gap: 2rem; padding-left: 1.2rem; } .toc a, a { color: #A5B4FC; text-decoration: none; } .toc a:hover, a:hover { text-decoration: underline; } .warn { background: rgba(245, 158, 11, 0.1); border-left-color: #F59E0B; } .warn .alert-title { color: #F59E0B; } .tag { display: inline-block; background: var(--code-bg); border: 1px solid var(--border); border-radius: 4px; padding: 0 0.4em; font-size: 0.75rem; color: var(--text-muted); } pre code { white-space: pre; } .flow { font-family: 'Fira Code', Consolas, monospace; font-size: 0.8rem; line-height: 1.5; } .cfgform { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 0.75rem; margin-top: 0.5rem; } .cfgform label { display: block; font-size: 0.8rem; font-weight: 600; color: var(--text-muted); } .cfgform input { width: 100%; margin-top: 0.25rem; padding: 0.5rem 0.65rem; background: var(--code-bg); color: var(--text); border: 1px solid var(--border); border-radius: 6px; font: inherit; font-size: 0.9rem; } .cfgform input:focus { outline: 2px solid var(--primary); outline-offset: 1px; } .snipgroup { margin-top: 0.5rem; } .tabs { display: flex; flex-wrap: wrap; gap: 0.25rem; border-bottom: 1px solid var(--border); } .tabs button { background: none; border: 0; color: var(--text-muted); padding: 0.5rem 0.8rem; cursor: pointer; font: inherit; font-size: 0.85rem; border-bottom: 2px solid transparent; margin-bottom: -1px; } .tabs button:hover { color: var(--text); } .tabs button[aria-selected="true"] { color: var(--text); border-bottom-color: var(--primary); font-weight: 600; } .tabs button:focus-visible, .copy-btn:focus-visible { outline: 2px solid #A5B4FC; outline-offset: 2px; } .tabpanel { position: relative; } .tabpanel .copy-btn { position: absolute; top: 0.6rem; right: 0.6rem; padding: 0.3rem 0.7rem; } .tabpanel pre { margin-top: 0.75rem; padding-top: 1rem; } @media (max-width: 640px) { body { padding: 1rem; } .toc { columns: 1; } }

Webchat v2

Documentação oficial de implementação: widget embutível, API pública e integração com o sistema de atendimento.

  1. Visão geral e fluxo
  2. Instalação rápida (copiar e colar)
  3. Instalação em 1 minuto
  4. Atributos do script
  5. API JavaScript
  6. Configuração no painel
  7. Referência de endpoints
  8. Webhook presence.update
  9. Interações v2.1
  10. Construindo um widget próprio
  11. Limites
  12. Segurança
  13. Retrocompatibilidade com a v1
  14. FAQ e solução de problemas

Visão geral e fluxo

O webchat conecta um chat no seu site ao sistema de atendimento. O visitante conversa pelo widget; o hub guarda a conversa, repassa cada mensagem ao sistema de atendimento por webhook e entrega as respostas dos atendentes de volta ao widget por polling. A v2 adiciona anexos, recibos de leitura, horário de atendimento, presença (online e digitando) e restauração de histórico.

visitante (widget)  --POST /api/webhooks/webchat/:token-->  hub  --webhook message.received-->  sistema de atendimento
visitante (widget)  <--GET  /api/webchat/messages (polling)--  hub  <--POST /api/messages/sendText|sendMedia--  atendente
visitante (widget)  --POST /api/webchat/presence-->  hub  --webhook presence.update-->  sistema ("digitando...")
atendente abre o ticket  --POST /api/messages/markAsRead-->  hub (status read)  -->  widget mostra "lida" (✓✓)

Convenções desta página: {API} é a URL pública do hub (ex.: https://sua-api.com.br), {token} é o token UUID da conexão webchat e x-api-key é a chave pública do widget (wc_...). O CORS é aberto, então o widget funciona em qualquer domínio.

Instalação rápida (copiar e colar)

Informe os dados da sua conexão e todos os exemplos abaixo são preenchidos na hora. Também é possível abrir esta página com ?token=SEU_TOKEN&key=wc_SUA_CHAVE (e &api=https://sua-api.com.br se a API estiver em outro domínio). Nada é enviado para fora do navegador.

A. Widget pronto (embed)

Uma linha no site. Escolha o seu ambiente:

B. Integração direta pela API (widget próprio ou bot)

Cada exemplo gera o visitorId (128 bits), envia uma mensagem, faz polling com lastMessageId, since e cursor, e mostra como responder, reagir, editar, apagar e avisar leitura. Contrato completo na seção Interações v2.1.

Instalação em 1 minuto

1

Crie a conexão

No painel, vá em Conexões e crie uma conexão do tipo webchat. Anote o token e a chave wc_... (a tela de configurações do webchat já mostra o snippet pronto).

2

Cole o snippet no seu site

Antes do fechamento de </body>:

Snippet do widget

<script
  src="https://sua-api.com.br/webchat/widget.js"
  data-token="SEU_TOKEN"
  data-api-key="wc_SUA_CHAVE"
  defer></script>

Pronto. O widget busca aparência, horário e limites no hub, então mudanças feitas no painel valem sem editar o site. Um arquivo único, sem dependências e sem build; usa Shadow DOM, então o CSS do seu site não interfere e o do widget não vaza.

Para testar manualmente: /webchat-demo?token=SEU_TOKEN&key=wc_SUA_CHAVE.

Atributos do script

AtributoPadrãoDescrição
data-tokenobrigatório-Token UUID da conexão.
data-api-keyobrigatório-Chave pública do widget (wc_...).
data-api-urlorigem do srcURL do hub, se o script for servido de outro lugar (CDN, proxy).
data-positionpainelright ou left; sobrepõe o painel.
data-colorpainelCor de destaque em hexadecimal (ex.: #0F766E). O contraste do texto é calculado sozinho.
data-titlepainelTítulo do cabeçalho.
data-collect-whatsappoptionalrequired, optional ou off. Quando preenchido, valida 10 a 15 dígitos.
data-open-true abre o chat ao carregar.
data-langpt-BRpt-BR, en ou es.
data-footer-false oculta o rodapé "Atendimento por ...".

Recursos incluídos: pré-chat (nome, WhatsApp e primeira mensagem), mensagem de boas-vindas, banner offline, status de envio (enviando, enviada, lida), anexos por clipe, arrastar e soltar e colar imagem, miniaturas com lightbox, vídeo, áudio e documentos, formatação *negrito*_itálico_~tachado~`código` e links, badge de não lidas com som discreto, tema escuro automático, tela cheia em telas abaixo de 480px e menu "Encerrar conversa". Nada é renderizado com innerHTML.

API JavaScript

MétodoDescrição
HubWebchat.open()Abre a janela.
HubWebchat.close()Fecha a janela.
HubWebchat.toggle()Alterna abrir e fechar.
HubWebchat.identify({ name, whatsapp })Pré-preenche o pré-chat (e os dados enviados) com o usuário logado no seu site.
HubWebchat.on(evento, fn)Eventos: message (nova resposta do atendente, recebe { id, text, mediaUrl, mediaType, fileName, timestamp }), open e close.
<button onclick="HubWebchat.open()">Fale conosco</button>
<script>
  window.addEventListener('load', function () {
    HubWebchat.identify({ name: 'Ana Souza', whatsapp: '5511999999999' });
    HubWebchat.on('message', function (m) { console.log('Nova resposta', m.text); });
  });
</script>

Chamadas feitas antes do widget terminar de carregar ficam em fila e são executadas assim que ele estiver pronto. O widget só cria window.HubWebchat no escopo global.

Configuração no painel

Em Conexões, abra as configurações da conexão webchat. Tudo é salvo por PUT /api/webchat/connections/:token/settings (JWT) e lido pelo widget a cada carregamento.

GrupoCampos
Aparênciatitle, color, avatarUrl, position (right|left), welcomeMessage.
Horário de atendimentobusinessHours: fuso (timezone) e faixas HH:MM por dia (mon a sun), início menor que fim, até 4 faixas por dia. null remove o horário.
ModoavailabilityMode: auto segue o horário (sem horário configurado, fica sempre online), online força online, offline força offline.
OfflineofflineMessage: texto exibido no banner quando offline. O visitante ainda pode enviar mensagem; ela fica na fila de atendimento.
curl -X PUT "https://sua-api.com.br/api/webchat/connections/SEU_TOKEN/settings" \
  -H "Authorization: Bearer SEU_JWT" -H "Content-Type: application/json" \
  -d '{
    "title": "Atendimento", "color": "#4F46E5",
    "welcomeMessage": "Olá! Como podemos ajudar?",
    "offlineMessage": "Estamos fora do horário. Deixe sua mensagem.",
    "availabilityMode": "auto",
    "businessHours": {
      "timezone": "America/Sao_Paulo",
      "days": { "mon": [["08:00","18:00"]], "tue": [["08:00","18:00"]], "wed": [["08:00","18:00"]],
                "thu": [["08:00","18:00"]], "fri": [["08:00","18:00"]], "sat": [["09:00","13:00"]], "sun": [] }
    }
  }'

GET /api/webchat/connections/:token/settings (JWT) devolve as configurações atuais, a apiKey, a URL do script e o snippet pronto. Quando offline em modo auto, o widget mostra "Offline · voltamos às HH:MM" no fuso do visitante, usando availability.nextOpenAt.

Referência de endpoints

Todos os endpoints públicos usam o header x-api-key, exceto a configuração do widget. Erros seguem { "error": { "code", "message" } } (ou { "error": "..." }).

1. Configuração do widget

GET/api/webchat/widget/:tokensem x-api-key
{
  "success": true,
  "config": {
    "title": "Atendimento", "color": "#4F46E5", "welcomeMessage": "Olá! Como podemos ajudar?",
    "businessName": "Empresa X", "avatarUrl": null, "position": "right",
    "offlineMessage": "Estamos fora do horário...",
    "availability": { "status": "online", "mode": "auto", "timezone": "America/Sao_Paulo", "nextOpenAt": null },
    "upload": { "enabled": true, "maxSizeBytes": 16777216, "accept": ["image/png", "application/pdf", "..."] },
    "polling": { "openIntervalMs": 3000, "idleIntervalMs": 15000 },
    "presence": { "heartbeatIntervalMs": 25000, "typingThrottleMs": 3000 }
  }
}

2. Upload de anexo

POST/api/webchat/upload/:token

Header x-api-key; corpo multipart/form-data com o campo file (um arquivo).

curl -X POST "https://sua-api.com.br/api/webchat/upload/SEU_TOKEN" \
  -H "x-api-key: wc_SUA_CHAVE" -F "file=@foto.png"
{ "success": true,
  "attachment": { "url": "https://sua-api.com.br/uploads/media/12/webchat/foto-169..-abc.png",
                  "mimeType": "image/png", "mediaType": "image", "fileName": "foto.png", "size": 48213 } }
StatusCódigoSignificado
400MISSING_FILENenhum arquivo no campo file.
401INVALID_API_KEYChave inválida.
413FILE_TOO_LARGEAcima do limite de tamanho.
415UNSUPPORTED_MEDIA_TYPETipo não aceito (SVG e HTML são sempre recusados).
429RATE_LIMIT_EXCEEDEDMuitos uploads.

mediaType: image, video, audio ou document.

3. Mensagem do visitante (texto e/ou anexo)

POST/api/webhooks/webchat/:token
CampoTipoDescrição
visitorIdstringIdentificador do visitante, ^[A-Za-z0-9_-]{8,128}$obrigatório
visitorNamestringNome do visitante.
whatsappstringWhatsApp com DDI e DDD, só dígitos.
messagestringTexto ou legenda, até 4096 caracteres. Opcional quando há attachment.
attachmentobject{ url, mimeType, fileName, mediaType }, exatamente como devolvido pelo upload.
clientMessageIdstringId gerado no cliente; é devolvido na resposta para casar a mensagem local.

É obrigatório informar visitorId e (message não vazia ou attachment). A attachment.url precisa ter sido emitida pelo upload desta conexão; qualquer outra URL retorna 400 INVALID_ATTACHMENT.

curl -X POST "https://sua-api.com.br/api/webhooks/webchat/SEU_TOKEN" \
  -H "Content-Type: application/json" -H "x-api-key: wc_SUA_CHAVE" \
  -d '{
    "visitorId": "visitor_3f9c1a7e5b2d4c8091a6e3f7d2b4c5a8",
    "visitorName": "Ana", "whatsapp": "5511999999999",
    "message": "Segue a foto",
    "attachment": { "url": "https://sua-api.com.br/uploads/media/12/webchat/foto-169..-abc.png",
                    "mimeType": "image/png", "fileName": "foto.png", "mediaType": "image" },
    "clientMessageId": "c_lr8x2k_1"
  }'
{ "ok": true, "chatId": 55, "messageId": 9012, "clientMessageId": "c_lr8x2k_1",
  "status": "received", "timestamp": "2026-10-17T12:00:00.000Z" }

Guarde o messageId na mensagem local: é com ele que você casa os recibos de leitura.

4. Polling de respostas

GET/api/webchat/messages/:token/:visitorId?lastMessageId=N
{
  "success": true,
  "messages": [
    { "id": 9013, "direction": "out", "text": "Olá Ana!", "type": "text", "mediaUrl": null, "mediaType": null,
      "mimeType": null, "fileName": null, "timestamp": "2026-10-17T12:00:05.000Z" },
    { "id": 9014, "direction": "out", "text": "segue o boleto", "type": "media",
      "mediaUrl": "https://sua-api.com.br/uploads/media/12/boleto-....pdf", "mediaType": "document",
      "mimeType": "application/pdf", "fileName": "boleto.pdf", "timestamp": "..." }
  ],
  "receipts": { "lastReadMessageId": 9012 },
  "availability": { "status": "online", "mode": "auto", "timezone": "America/Sao_Paulo", "nextOpenAt": null }
}

5. Histórico completo

GET/api/webchat/history/:token/:visitorId?beforeId=N&limit=50

limit de 1 a 100; ordem crescente. Use quando o widget já tem visitorId mas nenhuma mensagem local (outro navegador, storage limpo).

{ "success": true,
  "messages": [ { "id": 9012, "direction": "in", "text": "...", "type": "image", "mediaUrl": "...",
                  "mediaType": "image", "mimeType": "image/png", "fileName": "foto.png",
                  "status": "read", "timestamp": "..." } ],
  "hasMore": false, "receipts": { "lastReadMessageId": 9012 } }

Aqui direction: "in" é a mensagem do visitante e "out" a da empresa.

6. Presença do visitante

POST/api/webchat/presence/:token/:visitorId
curl -X POST "https://sua-api.com.br/api/webchat/presence/SEU_TOKEN/visitor_3f9c..." \
  -H "Content-Type: application/json" -H "x-api-key: wc_SUA_CHAVE" \
  -d '{ "state": "typing" }'

state: online, typing, paused ou offline. Resposta: { "success": true, "availability": { ... } }.

7. Marcar como lida (sistema de atendimento)

POST/api/messages/markAsReadtoken da empresa
{ "token": "TOKEN_DA_CONEXAO", "messageId": "wc_... ou id numérico" }

Para webchat, marca essa mensagem do visitante e todas as anteriores da mesma conversa como read. O widget passa a exibir ✓✓ via receipts.lastReadMessageId.

8. Respostas do atendente (sistema de atendimento)

As respostas saem pelos endpoints de envio padrão do hub, usando o visitorId como destinatário (o phoneNumber do chat): POST /api/messages/sendText e POST /api/messages/sendMedia. O hub detecta que a conexão é webchat, não envia via WhatsApp e deixa a mensagem disponível para o polling.

Atenção

Não use o WhatsApp real do visitante como destinatário. Use sempre o visitorId do chat, para não duplicar conversas.

Webhook presence.update (para o sistema de atendimento)

Quem integra o sistema de atendimento recebe, no webhook da conexão, o evento presence.update para exibir "digitando..." e online/offline do visitante:

{
  "event": "presence.update",
  "timestamp": "2026-10-17T12:00:00.000Z",
  "connection": { "id": "<token>", "channel": "webchat", "businessName": "Empresa X" },
  "chat": { "id": 55, "phoneNumber": "visitor_3f9c...", "visitorId": "visitor_3f9c...", "contactName": "Ana" },
  "presence": { "state": "composing", "lastSeenAt": "2026-10-17T12:00:00.000Z" }
}
presence.stateOrigem (state do widget)
composingtyping
pausedpaused
availableonline
unavailableoffline

Interações v2.1: responder, editar, apagar, reagir, digitando e leitura

Tudo aditivo: widgets v2 que ignoram os campos novos continuam funcionando. Ids são os do servidor (messageId do envio, id do polling e do histórico). Só mensagens que já têm id podem ser citadas, editadas, apagadas ou reagidas.

Formato público da mensagem (polling, histórico e updates)

{
  "id": 9013, "direction": "out", "text": "Olá Ana!", "...": "campos da v2",
  "replyTo": { "id": 9012, "direction": "in", "text": "preview até 140 chars", "mediaType": null, "deleted": false },
  "editedAt": "2026-10-17T12:01:00.000Z",
  "deleted": false,
  "welcome": false,
  "reactions": [ { "by": "agent", "emoji": "👍" }, { "by": "visitor", "emoji": "❤️" } ]
}

Polling com alterações, cursor e "digitando"

GET/api/webchat/messages/:token/:visitorId?lastMessageId=N&since=<cursor>
{
  "success": true,
  "messages": [ ... ],
  "updates": [ { "id": 9010, "...": "mensagem completa" } ],
  "cursor": "2026-10-17T12:01:03.000Z",
  "agentTyping": false,
  "receipts": { "lastReadMessageId": 9012 },
  "availability": { ... }
}

Responder (citar)

No POST /api/webhooks/webchat/:token envie "replyToId": 9013 (id de qualquer mensagem da mesma conversa). Id inexistente ou de outra conversa: 400 INVALID_REPLY_TO. O atendimento recebe message.rawData.context = { "id": "<whatsappMessageId da citada>" }.

Editar, apagar e reagir

Header x-api-key; :messageId é o id do servidor.

AçãoRotaCorpoRegras
EditarPATCH /api/webchat/messages/:token/:visitorId/:messageId{ "text": "novo" }Só do próprio visitante, não apagada, com texto, até 15 min. 1 a 4096 caracteres.
ApagarDELETE /api/webchat/messages/:token/:visitorId/:messageId-Só do próprio visitante, até 48 h. Confirme com o usuário antes (sem window.confirm em widgets embutidos).
ReagirPOST /api/webchat/messages/:token/:visitorId/:messageId/reaction{ "emoji": "👍" }Qualquer mensagem não apagada. "" remove. Até 16 caracteres. Tocar na própria reação atual deve removê-la.

Resposta 200: { "success": true, "message": { ...formato público } }; substitua a mensagem local pela devolvida. Erros: 404 MESSAGE_NOT_FOUND, 403 NOT_OWN_MESSAGE, 409 MESSAGE_DELETED, 409 EDIT_WINDOW_EXPIRED, 409 DELETE_WINDOW_EXPIRED, 400 INVALID_TEXT, 400 INVALID_EMOJI. O widget oficial mostra um aviso curto para cada um.

Repasse ao sistema de atendimento: edição chega como message.received com message.type = "edit" (rawData.edit.original_message_id), exclusão como type = "revoke" e reação como event: "message.reaction" (reaction.emoji = null quando removida).

Recibo de leitura para o atendente

POST/api/webchat/read/:token/:visitorId
{ "lastMessageId": 9014 }   // 200: { "success": true, "updated": 2 }

Marca como read as respostas da empresa com id <= lastMessageId (máx. 100 por chamada) e avisa o atendimento (message.status = read). Chame quando o painel estiver aberto, a aba visível (document.visibilityState === "visible") e houver resposta nova renderizada, no máximo uma vez a cada 2 s, também ao abrir o painel e ao voltar para a aba. Erros devem ser silenciosos (best effort).

Saudação (welcomeMessage)

config.welcomeMessage pode ser null/vazio: nesse caso não exiba nenhuma saudação. Quando definida, mostre-a localmente como prévia antes da conversa começar. Ao receber a primeira mensagem do visitante, o hub cria uma mensagem de saída (id menor que a do visitante) com "welcome": true, entregue pelo polling normal (lastMessageId=0) ou pelo histórico. Ao receber uma mensagem com welcome: true, descarte a prévia local para a saudação não aparecer duas vezes, mantendo a ordem do servidor (saudação primeiro). Ao restaurar o histórico com a saudação oficial, não monte a prévia.

Ações do atendente (sistema de atendimento para o hub)

Com o token da empresa; token é o da conexão e to é o visitorId.

AçãoRotaCorpo
DigitandoPOST /api/messages/sendPresence{ "token", "to", "state": "composing" | "recording" | "paused" } (vale 10 s; reenvie a cada ~5 s)
EditarPOST /api/messages/edit{ "token", "to", "messageId", "text" }
ApagarPOST /api/messages/delete{ "token", "to", "messageId" }
ReagirPOST /api/messages/sendReaction{ "token", "to", "reactionMessageId", "reactionEmoji" } ("" remove)
RespondersendText / sendMediacontext: { "message_id": "<wid da citada>" }

Editar e apagar só valem para mensagens da empresa (direction: out). Reação e resposta no webchat não geram mensagem nova: a reação vai para reactions da mensagem alvo e a citação para replyTo.

Construindo um widget próprio

Se preferir uma interface própria, o ciclo é: gerar visitorId, enviar mensagens, fazer polling, e opcionalmente anexos e presença.

Exemplo com curl

# 1) configuração
curl "https://sua-api.com.br/api/webchat/widget/SEU_TOKEN"
# 2) enviar mensagem
curl -X POST "https://sua-api.com.br/api/webhooks/webchat/SEU_TOKEN" \
  -H "Content-Type: application/json" -H "x-api-key: wc_SUA_CHAVE" \
  -d '{"visitorId":"visitor_3f9c1a7e5b2d4c8091a6e3f7d2b4c5a8","visitorName":"Ana","message":"Olá"}'
# 3) buscar respostas
curl "https://sua-api.com.br/api/webchat/messages/SEU_TOKEN/visitor_3f9c1a7e5b2d4c8091a6e3f7d2b4c5a8?lastMessageId=0" \
  -H "x-api-key: wc_SUA_CHAVE"

Exemplo com JavaScript

const API = 'https://sua-api.com.br';
const TOKEN = 'SEU_TOKEN';
const KEY = 'wc_SUA_CHAVE';
const headers = { 'x-api-key': KEY, 'Content-Type': 'application/json' };
// visitorId com 128 bits, guardado por conexão
function getVisitorId() {
  const k = 'wc_visitor_' + TOKEN;
  let id = localStorage.getItem(k);
  if (!id) {
    const b = crypto.getRandomValues(new Uint8Array(16));
    id = 'visitor_' + Array.from(b, x => x.toString(16).padStart(2, '0')).join('');
    localStorage.setItem(k, id);
  }
  return id;
}
async function sendText(text, name) {
  const res = await fetch(`${API}/api/webhooks/webchat/${TOKEN}`, {
    method: 'POST', headers,
    body: JSON.stringify({ visitorId: getVisitorId(), visitorName: name, message: text,
                           clientMessageId: 'c_' + Date.now() })
  });
  return res.json(); // { ok, messageId, clientMessageId, ... }
}
async function sendFile(file, caption) {
  const fd = new FormData();
  fd.append('file', file);
  const up = await fetch(`${API}/api/webchat/upload/${TOKEN}`, {
    method: 'POST', headers: { 'x-api-key': KEY }, body: fd   // não defina Content-Type
  }).then(r => r.json());
  return fetch(`${API}/api/webhooks/webchat/${TOKEN}`, {
    method: 'POST', headers,
    body: JSON.stringify({ visitorId: getVisitorId(), message: caption || '', attachment: up.attachment })
  }).then(r => r.json());
}
let last = 0;
async function poll() {
  const res = await fetch(`${API}/api/webchat/messages/${TOKEN}/${getVisitorId()}?lastMessageId=${last}`,
                          { headers: { 'x-api-key': KEY } });
  const data = await res.json();
  for (const m of data.messages || []) {
    last = Math.max(last, m.id);
    // renderize com textContent, nunca com innerHTML
  }
  // data.receipts.lastReadMessageId => marque como lidas as suas mensagens com messageId <= esse valor
  setTimeout(poll, 3000);
}
poll();
Boas práticas

Pause o polling com a página oculta (visibilitychange), use backoff exponencial em erro (até 60 s), deduplique mensagens por id, valide tipo e tamanho do arquivo antes do upload e nunca insira texto do servidor com innerHTML.

Limites

ItemLimite
Tamanho de anexo16 MB por padrão (upload.maxSizeBytes na configuração)
Tipos aceitosLista em upload.accept: imagens (JPEG, PNG, GIF, WebP), vídeo (MP4, WebM, QuickTime), áudio (MP3, MP4, OGG, WebM, WAV, AAC, M4A), PDF, Word, Excel, PowerPoint, TXT, CSV e ZIP. SVG e HTML são recusados.
Rate limit de upload30 uploads a cada 10 minutos por conexão e IP (429)
Texto da mensagem4096 caracteres
PollingMáx. 50 mensagens por chamada; histórico de 1 a 100 por página
Histórico local do widget200 mensagens por token

Segurança

Retrocompatibilidade com a v1

FAQ e solução de problemas

O botão do chat não aparece.

Confira no console do navegador se há o aviso [HubWebchat]. Causas comuns: data-token ou data-api-key ausentes, token incorreto (a rota de configuração responde 404) ou script bloqueado por CSP. Se o seu site usa Content-Security-Policy, libere o domínio do hub em script-src, connect-src (e img-src/media-src para anexos).

Recebo 401 INVALID_API_KEY.

A chave não corresponde ao token informado. Copie novamente o snippet nas configurações da conexão.

O widget aparece como offline, mas era para estar online.

Verifique o modo (availabilityMode) e o fuso (timezone) em businessHours. O modo offline força offline mesmo dentro do horário. Mesmo offline o visitante pode enviar mensagens.

As respostas do atendente não chegam.

Responda usando o visitorId do chat como destinatário. O widget só busca mensagens com polling (a cada 3 s com o chat aberto e 15 s fechado) e pausa quando a aba está oculta; ao voltar para a aba, atualiza na hora.

As mensagens nunca ficam com ✓✓.

A leitura depende do atendente abrir o ticket e o sistema chamar POST /api/messages/markAsRead. O widget só marca como lida a mensagem com messageId menor ou igual a receipts.lastReadMessageId.

O upload falha.

413: arquivo acima do limite; 415: tipo não aceito; 429: aguarde alguns minutos. Se estiver atrás de proxy, confira o limite de corpo (client_max_body_size no nginx) e que o domínio dos anexos é acessível pelo visitante.

O histórico sumiu em outro navegador.

O visitorId fica no localStorage de cada navegador. Outro navegador ou aparelho é outro visitante. Sem storage (modo privado restrito) o widget funciona, mas a conversa não sobrevive ao recarregamento.

Posso ter dois chats na mesma página?

Não. O widget carrega uma única vez por página; um segundo script é ignorado.

Como personalizo o visual?

Cor, título, avatar, posição e mensagens vêm do painel (ou de data-*). O CSS está encapsulado em Shadow DOM, por isso não é sobrescrito por estilos do site.