Widget
O widget de suporte do PumaHelp em uma tag de script. O visitante abre conversas, acompanha as respostas em tempo real, anexa arquivos e avalia o atendimento — sem sair do seu site.
<script type="module"
src="https://cdn.pumahelp.com/widget/v3/widget.js"
data-app="acme"
data-api-key="rk_live_..."></script>
Não há segunda tag, não há chamada de função, não há espera. O widget se inicializa sozinho lendo os
data-* da própria tag.
O type="module" é obrigatório. O widget carrega o painel sob demanda, e é o modo módulo que faz
esse carregamento resolver contra o CDN. Sem ele, o navegador nem chega a executar o script.
Módulo já carrega adiado por definição — não é preciso defer.
🚀 Instalação
O básico
Troque acme pelo subdomínio da sua organização e cole a chave restrita:
<script type="module"
src="https://cdn.pumahelp.com/widget/v3/widget.js"
data-app="acme"
data-api-key="rk_live_..."></script>
O botão flutuante aparece no canto inferior direito. É só isso.
Embutido num container
Sem botão flutuante, ocupando todo o espaço de um elemento seu — para uma página de ajuda ou uma aba de suporte dentro do seu produto:
<div id="suporte" style="height: 600px"></div>
<script>
window.pumahelp = window.pumahelp || function () { (pumahelp.q = pumahelp.q || []).push(arguments) };
pumahelp('init', {
appName: 'acme',
apiKey: 'rk_live_...',
container: document.getElementById('suporte'),
});
</script>
<script type="module" src="https://cdn.pumahelp.com/widget/v3/widget.js"></script>
Passar container implica embedded: true.
A fila de comandos
Repare que o pumahelp(...) do exemplo acima vem antes da tag do script. Isso é intencional e
funciona: aquela primeira linha instala uma fila que guarda tudo o que você chamar, e o widget a
consome assim que carrega.
<script>
window.pumahelp = window.pumahelp || function () { (pumahelp.q = pumahelp.q || []).push(arguments) };
pumahelp('on', 'pumahelp:ticket-created', ({ ticketId }) => {
analytics.track('suporte_conversa_aberta', { ticketId });
});
</script>
Você nunca precisa esperar o widget carregar, nem testar se window.pumahelp já existe.
🔐 Autenticação
Há dois modos. Eles resolvem problemas diferentes e podem conviver.
| Modo | Como se ativa | Quando usar |
|---|---|---|
| Chave restrita | data-api-key | Site sem backend, ou visitante anônimo |
| Provedor de token | pumahelp('auth', fn) | Você tem backend e não quer segredo nenhum no HTML |
Chave restrita
A chave vai no HTML e é pública por definição — qualquer visitante a lê no código-fonte. Por isso ela precisa ser restrita: crie uma chave de API com o escopo de criação de convidado e nada além disso.
Nunca use no widget uma chave com escopo de leitura de tickets, de usuários ou de relatórios. Uma chave no navegador é uma chave publicada — trate o escopo como o único limite real.
Provedor de token
Se você tem backend, existe caminho melhor: o widget pede o token ao seu servidor, e nenhum segredo chega ao navegador.
<script>
window.pumahelp = window.pumahelp || function () { (pumahelp.q = pumahelp.q || []).push(arguments) };
pumahelp('auth', async () => {
const response = await fetch('/api/suporte/token'); // endpoint SEU
const { token } = await response.json();
return token;
});
</script>
<script type="module" src="https://cdn.pumahelp.com/widget/v3/widget.js" data-app="acme"></script>
Repare que não há data-api-key. O widget chama essa função na inicialização e outra vez sempre que
a credencial expirar — ela pode ser async, e devolver null se não houver usuário.
Porque a função é chamada de novo a cada expiração e a cada carregamento de página, o provedor é o
único jeito de a identidade sobreviver sozinha. Se o seu site tem login, prefira-o ao
identify().
Se preferir registrar o provedor junto da configuração, init() aceita o mesmo:
pumahelp('init', {
appName: 'acme',
auth: async () => (await fetch('/api/suporte/token')).json().then((r) => r.token),
});
No seu servidor, emita o token com a chave que fica no servidor:
// POST /api/suporte/token — no SEU backend
const response = await fetch('https://acme.pumahelp.com/api/v1/users/impersonate', {
method: 'POST',
headers: {
'X-API-Key': process.env.PUMAHELP_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: usuario.nome,
email: usuario.email,
external_id: usuario.id,
role: 'end-user',
}),
});
const { access_token } = await response.json();
Assim a conversa já nasce vinculada ao usuário certo, e o histórico o acompanha entre dispositivos e
navegadores. A chave usada aqui precisa do escopo user:impersonate e nunca deve ir para o
navegador. Veja a referência da API para o contrato completo do endpoint.
Identificar depois
Se a pessoa faz login já com o widget na tela, não é preciso recarregar nada:
pumahelp('identify', { token: '<jwt emitido pelo seu backend>' });
E ao sair:
pumahelp('logout');
identify() vale por carregamento de páginaO token do impersonate não tem refresh, e o widget não o guarda — gravá-lo deixaria uma
credencial de longa duração no armazenamento local do seu site.
Na prática: chame identify() a cada carregamento, ou registre um provedor de token e não se
preocupe mais com isso. Sem nenhum dos dois, a pessoa volta como visitante anônima depois de um F5 ou
quando o token expirar — e nesse momento o widget emite
pumahelp:session-lost, que é o seu gancho para reidentificar:
pumahelp('on', 'pumahelp:session-lost', async () => {
const { token } = await fetch('/api/suporte/token').then((r) => r.json());
pumahelp('identify', { token });
});
O logout descarta a sessão local. As conversas continuam existindo na sua conta do PumaHelp — quem
entrar de novo com o mesmo usuário volta a vê-las.
⚙️ Configuração
Todo campo existe como atributo data-* na tag do script e como propriedade em init().
| Atributo | Propriedade | Padrão | O que faz |
|---|---|---|---|
data-app | appName | — | Obrigatório. Subdomínio da organização. Aceita letras, números e hífen |
data-api-key | apiKey | — | Chave restrita. Dispensável quando você registra um provedor de token |
data-theme | theme | auto | light, dark ou auto (segue o sistema do visitante) |
data-language | language | pt-BR | Idioma dos textos e das datas |
data-color | color | #f97116 | Cor de destaque, a mesma nos dois temas. Aceita qualquer cor CSS. O texto que fica em cima dela é escolhido pelo contraste — veja Aparência |
data-icon | icon | puma | Ícone do botão flutuante: puma, chat, question, bell, smile, lines ou send — ou a URL de uma imagem sua. Veja Ícone do botão |
data-embedded | embedded | false | Embute no lugar de flutuar |
data-z-index | zIndex | 2147483000 | Camada do widget |
data-sound | soundEnabled | true | Geral: false silencia os dois sons abaixo |
data-send-sound | sendSoundEnabled | true | Som ao enviar uma mensagem ou abrir uma conversa |
data-receive-sound | receiveSoundEnabled | true | Som ao receber uma resposta |
data-persist-session | persistSession | true | false mantém a sessão só em memória |
data-branding | branding | true | Link "Criado com PumaHelp" no pé do painel. false esconde |
data-collect-email | collectEmail | false | Convida o visitante anônimo a acompanhar a conversa por e-mail. Veja E-mail |
data-collect-rating | collectRating | true | Pergunta a satisfação quando a conversa é resolvida ou encerrada. false não pergunta. Veja Avaliação |
data-rating-comment | ratingComment | true | Oferece um campo de comentário depois da nota. false recolhe o cartão assim que a nota é dada |
data-auto-focus | autoFocus | true | Leva o foco ao campo de mensagem ao abrir o painel e ao entrar numa conversa. false deixa o foco no título. Veja Acessibilidade |
data-launcher-label | launcherLabel | — | Texto ao lado do ícone do botão flutuante; o botão vira uma pílula. Veja Texto e tamanho do botão |
data-translations | translations | — | JSON com textos sobrescritos. JSON inválido não impede o widget de abrir: ele cai nos textos padrão e emite pumahelp:error com field: "translations" |
data-tags | tags | — | Etiquetas aplicadas a todo ticket aberto por esta instalação. No atributo, separadas por vírgula (site,checkout); em init(), uma lista de textos. Para etiquetar por ticket, veja Etiquetar tickets |
| — | container | — | Elemento onde embutir. Só em init(). Implica embedded: true |
| — | auth | — | Provedor de token. Em init() ou via pumahelp('auth', fn) |
Booleanos
Valem como falso: false, 0, no, off e string vazia. Qualquer outro valor é verdadeiro, e o
atributo ausente cai no padrão.
<script type="module" src="…/widget.js" data-app="acme" data-api-key="rk_live_..."
data-sound="false"></script>
Erros de configuração
Configuração inválida não derruba a página: o widget emite pumahelp:error com o campo culpado e
não monta.
pumahelp('on', 'pumahelp:error', ({ field, message }) => {
console.warn('Widget não iniciou:', field, message);
});
📋 API programática
window.pumahelp recebe um comando e seus argumentos.
pumahelp('open'); // abre o painel
pumahelp('close'); // fecha
pumahelp('toggle'); // alterna
pumahelp('logout'); // descarta a sessão local
pumahelp('update', { collectEmail: true }); // troca opções com o widget no ar
pumahelp('destroy'); // remove o widget da página
pumahelp('identify', { token: '<jwt>' });
pumahelp('auth', async () => (await fetch('/api/suporte/token')).json().then(r => r.token));
pumahelp('init', { appName: 'acme', apiKey: 'rk_live_...' });
| Comando | Argumentos | Devolve |
|---|---|---|
init | Config | A instância criada |
auth | () => string | null | Promise<string | null> | — |
open / close / toggle | — | — |
identify | { token } | Promise<void> — ou nada, quando enfileirado |
logout | — | — |
update | objeto com opções | — |
destroy | — | — |
on | evento, ouvinte | Função que cancela a inscrição |
Todos funcionam antes de o script carregar, graças à fila — inclusive on, cuja função de cancelar
já vale mesmo tendo sido pedida antes de o widget existir.
Trocar opções com o widget no ar
update muda a configuração sem recarregar nem recriar o widget. Aceita icon, collectEmail,
collectRating, ratingComment, autoFocus, launcherLabel, branding, tags, soundEnabled, sendSoundEnabled,
receiveSoundEnabled, theme, color, zIndex, language e translations. O que define a instância — appName, apiKey, auth, embedded,
container, persistSession — não muda por aqui: a chave é recusada com um pumahelp:error
(field diz qual), e nada mais é alterado. Chamado antes de o widget montar, vale como init().
pumahelp('update', { theme: 'dark', color: '#f26522' });
O que a fila não consegue devolver é um valor que ainda não existe: um identify chamado antes de o
widget montar devolve undefined em vez da promessa, e um init enfileirado devolve a instância
só quando ela é criada. Se você precisa da referência na hora, chame depois do carregamento.
Ouvir e parar de ouvir
const parar = pumahelp('on', 'pumahelp:unread-changed', ({ count }) => {
document.title = count > 0 ? `(${count}) Minha loja` : 'Minha loja';
});
// mais tarde
parar?.();
O retorno é opcional (parar?.()) porque, se você chamar on antes de o widget montar, a inscrição
entra pela fila e o cancelamento chega depois.
🎧 Eventos
Todo evento é um CustomEvent que atravessa o Shadow DOM e sobe até o document. Você pode ouvir
pelos dois caminhos — pumahelp('on', ...) ou document.addEventListener(...).
Todos são prefixados com pumahelp:, e o detalhe vai sempre em event.detail.
| Evento | event.detail |
|---|---|
pumahelp:ready | { instanceId: string } |
pumahelp:error | { field?: string, message: string, status?: number } |
pumahelp:open | — |
pumahelp:close | — |
pumahelp:destroy | — |
pumahelp:before-ticket-create | { subject: string, body: string, tags: string[] } — mutável; veja Etiquetar tickets |
pumahelp:ticket-created | { ticketId: number, subject: string } |
pumahelp:ticket-updated | { ticketId: number, status: string } |
pumahelp:message-sent | { ticketId: number } |
pumahelp:rated | { ticketId: number, score: string } |
pumahelp:unread-changed | { count: number } |
pumahelp:identified | { userId: string | null } |
pumahelp:session-lost | { reason: 'expired' } |
pumahelp:realtime | { state: 'connected' | 'reconnecting' | 'disconnected' } |
O widget não emite nenhum evento além destes.
pumahelp:error traz status quando a falha veio da API: 0 para rede fora, 5xx para erro do
servidor, 401 para credencial que morreu. Sem status, é erro de configuração — e aí field diz
qual campo. Erros de validação e limite de taxa não geram evento: o primeiro o painel já mostra no
lugar certo, e o segundo o widget resolve sozinho respeitando o Retry-After.
pumahelp:session-lost avisa que uma identidade declarada por identify() expirou e a conversa
seguirá anônima. Quem registra um provedor de token não recebe este evento — lá a renovação é
automática.
Analytics
document.addEventListener('pumahelp:ticket-created', (event) => {
gtag('event', 'suporte_conversa_aberta', {
ticket_id: event.detail.ticketId,
subject: event.detail.subject,
});
});
document.addEventListener('pumahelp:rated', (event) => {
gtag('event', 'suporte_avaliado', { score: event.detail.score });
});
Indicador de não lidas no seu próprio layout
pumahelp('on', 'pumahelp:unread-changed', ({ count }) => {
const badge = document.querySelector('#meu-badge-suporte');
badge.hidden = count === 0;
badge.textContent = String(count);
});
Avisar quando a conexão cair
pumahelp('on', 'pumahelp:realtime', ({ state }) => {
if (state === 'disconnected') mostrarAviso('Suporte offline, reconectando…');
if (state === 'connected') esconderAviso();
});
Etiquetar tickets
Há dois jeitos de um ticket aberto pelo widget já chegar etiquetado ao painel.
Etiquetas fixas valem para toda a instalação — úteis para saber de onde o ticket veio:
<script type="module" src="…/widget.js" data-app="acme" data-api-key="rk_live_..."
data-tags="site,checkout"></script>
Em init(), é tags: ['site', 'checkout'].
Etiquetas por ticket vêm do evento pumahelp:before-ticket-create, disparado logo antes do envio.
O event.detail é o rascunho do ticket — { subject, body, tags } — e ele é mutável: o que você
alterar ali é o que vai para a API. As etiquetas fixas já estão na lista quando o evento dispara.
pumahelp('on', 'pumahelp:before-ticket-create', (rascunho) => {
rascunho.tags.push(`plano-${usuario.plano}`);
if (usuario.vip) rascunho.tags.push('vip');
});
Pelo document funciona igual: event.detail.tags.push('vip'). Faça as alterações de forma
síncrona — o widget lê o rascunho assim que os ouvintes retornam.
Antes de enviar, o widget apara os espaços, remove repetidas e descarta o que a API não aceita
(etiqueta vazia ou com mais de 50 caracteres) — cada descarte gera um pumahelp:error com
field: "tags", e o ticket é enviado normalmente com as demais.
O evento dispara apenas quando o visitante abre uma conversa nova. A conversa de acompanhamento — aberta ao responder uma conversa encerrada — herda as etiquetas da original, e as respostas dentro de uma conversa não carregam etiquetas.
🎨 Aparência
O widget vive num Shadow DOM: o CSS do seu site não entra e o do widget não vaza. A customização
acontece por dois caminhos combináveis — variáveis CSS e ::part().
Cor de destaque
O caminho mais curto, e o que resolve a maioria dos casos:
<script type="module" src="…/widget.js" data-app="acme" data-api-key="rk_live_..."
data-color="#0ea5e9"></script>
Sem data-color, o destaque é o laranja da PumaHelp, #f97116 — o mesmo do painel, e o mesmo
nos dois temas.
O destaque pinta o que é pequeno e quer atenção: o botão flutuante, o botão de enviar, a nota
escolhida na avaliação. O balão do visitante usa um tom dele — --pw-bubble-user —, porque uma
conversa inteira em cor cheia cansa de ler.
O texto sobre a cor de destaque
--pw-accent-contrast é a cor de tudo o que o widget desenha em cima do destaque: o ícone do
botão flutuante, o rótulo dos botões e o texto do balão do visitante.
Você não precisa se preocupar com ela. Ao informar data-color, o widget mede a cor e usa
quase-preto ou branco, o que for mais legível — um destaque amarelo recebe texto preto, um azul
escuro recebe branco. Para discordar dessa escolha, uma linha no CSS do seu site vence:
pumahelp-widget::part(root) { --pw-accent-contrast: #ffffff; }
Ícone do botão
O botão flutuante traz a marca da PumaHelp. Para trocar por um símbolo neutro — útil quando a sua página já usa um balão de conversa para outra coisa:
<script type="module" src="…/widget.js" data-app="acme" data-api-key="rk_live_..."
data-icon="question"></script>
Os valores são puma (o padrão), chat, question, bell, smile, lines e send. Um nome
fora dessa lista não impede o widget de abrir: ele mantém o padrão e emite pumahelp:error com
field: "icon". Também dá para trocar com o widget no ar:
pumahelp('update', { icon: 'bell' });
A sua logo
data-icon também aceita a URL de uma imagem — a logo da sua empresa no lugar do ícone:
<script type="module" src="…/widget.js" data-app="acme" data-api-key="rk_live_..."
data-icon="https://acme.com/logo.svg"></script>
Uma imagem quadrada, com fundo transparente, em SVG ou PNG. Ela aparece com metade do tamanho do
botão (28px num botão de 56px), centrada, e o X de fechar toma o lugar dela quando o painel abre.
Para outro tamanho ou arredondamento, a parte launcher-image:
pumahelp-widget::part(launcher-image) { width: 34px; height: 34px; }
Se a imagem não carregar, o botão mostra o ícone padrão e o widget emite pumahelp:error com
field: "icon". Endereços que não são de imagem (javascript:, por exemplo) são recusados como um
nome fora da lista. pumahelp('update', { icon: 'https://…' }) troca no ar, e 'bell' volta ao
ícone embutido.
Em modo embutido não há botão flutuante, e o ícone não tem efeito.
Texto e tamanho do botão
Um texto ao lado do ícone transforma o botão numa pílula — "Fale conosco", "Assistente":
<script type="module" src="…/widget.js" data-app="acme" data-api-key="rk_live_..."
data-launcher-label="Fale conosco"></script>
Duas ou três palavras: o botão cresce com o texto, com uma transição suave. O texto é também o nome do botão para leitores de tela. Troca-se com o widget no ar, e vazio volta ao botão redondo:
pumahelp('update', { launcherLabel: 'Fale conosco' });
pumahelp('update', { launcherLabel: '' });
Com o painel aberto o texto se recolhe e o botão é só o de fechar; ao fechar, a pílula volta. O
texto aparece em qualquer tamanho de tela. Para deixar só o ícone no celular, esconda a parte
launcher-label:
@media (max-width: 480px) {
pumahelp-widget::part(launcher-label) { display: none; }
}
O tamanho do botão é a variável --pw-launcher-size (56px por padrão); o ícone e o contador de
não lidas acompanham:
pumahelp-widget::part(root) { --pw-launcher-size: 48px; }
Em modo embutido não há botão flutuante: texto e tamanho não têm efeito.
Paleta completa
As variáveis vivem na parte root, que é a raiz do widget:
pumahelp-widget::part(root) {
--pw-accent: #0ea5e9;
--pw-accent-contrast: #ffffff;
--pw-radius: 8px;
}
--pw-font é a exceçãoEla é a única variável lida fora da parte root, no próprio elemento. Declará-la em
::part(root) não tem efeito nenhum: o valor chega tarde demais na árvore. Use o seletor do
elemento:
pumahelp-widget {
--pw-font: 'Inter', system-ui, sans-serif;
}
| Variável | Padrão (claro) | Padrão (escuro) | O que pinta |
|---|---|---|---|
--pw-accent | #f97116 | — | Cor de destaque: botão flutuante, botão de enviar, nota escolhida |
--pw-accent-contrast | #140801 | — | Texto e ícones sobre a cor de destaque |
--pw-accent-strong | destaque 78% com preto | destaque 85% com branco | Anel de foco, borda do botão flutuante, seta de enviar acesa |
--pw-accent-tint | destaque a 14% sobre branco | destaque a 22% sobre o painel | Áreas grandes: balão do visitante e nota escolhida |
--pw-bg | #ffffff | #1c1a19 | Fundo do painel |
--pw-bg-subtle | #faf8f6 | #262321 | Fundos secundários: cabeçalho, campos, cartões |
--pw-fg | #1c1917 | #ece8e4 | Texto principal |
--pw-fg-muted | #78716c | #a8a29e | Texto secundário: datas, legendas |
--pw-border | #eae5e1 | #3a3532 | Bordas e divisórias |
--pw-bubble-agent | #f4f1ee | #2b2725 | Balão de quem atende |
--pw-bubble-user | --pw-accent-tint | — | Balão do visitante |
--pw-bubble-user-fg | --pw-fg | — | Texto do balão do visitante |
--pw-danger | #dc2626 | #f87171 | Erros e ações destrutivas |
--pw-success | #16a34a | #4ade80 | Confirmações |
--pw-radius | 16px | — | Arredondamento |
--pw-shadow | sombra suave | mais densa | Sombra do painel e do botão |
--pw-badge | #ef4444 | — | Contador de não lidas sobre o botão |
--pw-launcher-size | 56px | — | Tamanho do botão flutuante; ícone e contador acompanham |
--pw-font | pilha do sistema | — | Família tipográfica — só no seletor do elemento, não em ::part(root) |
--pw-z | 2147483000 | — | Camada. Prefira data-z-index |
Partes
Para ajustes que variáveis não alcançam:
pumahelp-widget::part(launcher) { inset-inline-end: 32px; inset-block-end: 32px; }
pumahelp-widget::part(header) { background: #0f172a; }
pumahelp-widget::part(send) { border-radius: 999px; }
pumahelp-widget::part(brand) { color: #94a3b8; }
pumahelp-widget::part(rating) { border-radius: 4px; }
| Parte | O que é |
|---|---|
root | Raiz do widget. É onde ficam as variáveis de tema |
launcher | Botão flutuante |
badge | Contador de não lidas sobre o botão |
launcher-label | Texto ao lado do ícone do botão flutuante. Vazio sem data-launcher-label |
launcher-image | A sua logo no botão flutuante, quando data-icon é uma URL |
surface | Painel da conversa |
header | Cabeçalho do painel |
body | Área rolável das mensagens |
footer | Rodapé do painel |
composer | O formulário de escrever — a área toda, não o campo de texto, que não tem parte própria |
send | Botão de enviar |
brand | Link "Criado com PumaHelp" no pé do painel |
rating | Cartão de avaliação. Vale tanto para a pergunta quanto para o "Você avaliou: …" |
rating-option | Cada uma das três opções de nota |
rating-option-selected | A opção escolhida. Vem junto de rating-option, nunca sozinha |
rating-comment | Campo de comentário da avaliação |
rating-submit | Botão de enviar a avaliação |
::part() alcança o elemento inteiro, mas não os filhos dele — não existe
::part(header) .titulo. Se o que você precisa não está na lista, o caminho é uma variável CSS.
Também não existe seletor de atributo depois de ::part(): ::part(rating-option)[aria-pressed]
não é CSS válido. É por isso que a opção escolhida carrega um segundo nome — estilize o estado com
::part(rating-option-selected).
Claro e escuro
Com data-theme="auto" (o padrão) o widget segue o sistema do visitante e troca junto quando ele
troca. Para fixar, use light ou dark.
Para dar valores diferentes em cada tema, use a media query normal — o widget não exige nada especial:
pumahelp-widget::part(root) { --pw-accent: #0ea5e9; }
@media (prefers-color-scheme: dark) {
pumahelp-widget::part(root) { --pw-accent: #38bdf8; }
}
🌍 Textos e idiomas
Vêm prontos pt-BR (padrão), en e es:
<script type="module" src="…/widget.js" data-app="acme" data-api-key="rk_live_..."
data-language="en"></script>
Qualquer texto pode ser sobrescrito, em qualquer idioma. O que você não informar cai no idioma escolhido, e o que faltar nele cai no pt-BR — nunca fica em branco.
pumahelp('init', {
appName: 'acme',
apiKey: 'rk_live_...',
translations: {
title: 'Fale com a gente',
welcomeHint: 'Qual é a sua dúvida? Escreva aqui e a gente responde.',
ratingQuestion: 'A gente resolveu o seu problema?',
},
});
Em HTML, o mesmo objeto vai como JSON no atributo:
<script type="module" src="…/widget.js" data-app="acme" data-api-key="rk_live_..."
data-translations='{"title":"Fale com a gente"}'></script>
Chaves disponíveis:
| Grupo | Chaves |
|---|---|
| Geral | title, close, back, retry, offline |
| Lista | listError, listLoadMore, listClosed, newConversation, previewYou |
| Nova conversa | welcome, welcomeHint, newPlaceholder, newError |
| Conversa | chatError, chatPlaceholder, chatPlaceholderOffline, chatSend, chatTyping, chatReceived |
| Mensagem | messageSending, messageFailed, discard, unreadOne, unreadMany |
| Anexos | attach, attachments, attachmentsPending, attachmentUploading, attachmentRemove, attachmentsTooMany, attachmentError |
| Avaliação | ratingQuestion, ratingGood, ratingNeutral, ratingBad, ratingCommentPlaceholder, ratingSubmit, ratingThanks, ratingGiven, ratingChange, ratingCancel, ratingError |
emailInvite, emailInviteHint, emailPlaceholder, emailSubmit, emailDismiss, emailSent, emailCodeLabel, emailCodeSubmit, emailResend, emailResendIn, emailCodeInvalid, emailCodeExpired, emailTooMany, emailUnavailable, emailChange, emailError, emailConfirmed | |
| Situação | statusSolved, statusClosed |
| Notificação do navegador | notificationTitle, notificationBody |
| Botão flutuante | launcherOpen, launcherClose — o nome do botão para leitores de tela quando não há data-launcher-label; unreadOne e unreadMany também entram na contagem anunciada |
Três textos têm um marcador que o widget preenche: emailSent recebe o endereço em {email},
emailResendIn os segundos que faltam em {seconds} e attachmentsTooMany o limite de arquivos em
{max}. Ao sobrescrevê-los, mantenha o marcador — sem ele, o texto aparece sem essa informação.
💬 O que o widget faz
Tempo real
As respostas de quem atende chegam sozinhas, sem recarregar nem consultar em intervalos. O estado da
conexão é observável pelo evento pumahelp:realtime, e a reconexão é automática. Conversas longas
carregam o histórico aos poucos, a partir do topo, e os dias ficam separados ("Hoje", "Ontem", data).
Com a aba em segundo plano, a resposta também vira uma notificação do navegador — quando o site
tem permissão de notificações, que o widget só pede no momento em que o visitante escolhe acompanhar
a conversa por e-mail, nunca por conta própria. Os textos são as chaves notificationTitle e
notificationBody em data-translations.
O widget conecta na primeira vez que o painel é aberto e mantém a conexão depois, mesmo com o painel fechado — é o que faz o contador do botão subir quando a equipe responde. Antes do primeiro clique não há conexão: quem volta com sessão vê o contador pela contagem do servidor, sem tempo real.
Anexos
O visitante anexa pelo clipe ou colando com Ctrl+V — não há arrastar e soltar. Até
20 arquivos por mensagem, de até 50 MB cada. Os dois limites são recusados na hora, com
aviso: nada sobe para falhar depois. Um arquivo pode ir sozinho, sem texto — uma foto do
problema é uma mensagem completa, e na lista de conversas ela aparece como 📎 nome-do-arquivo.
Envio otimista e fila offline
A resposta aparece na conversa assim que a pessoa envia, marcada como "enviando". Se a rede cair, ela fica marcada como não enviada e sai sozinha quando a conexão voltar — nada do que foi escrito se perde.
Reenviar não duplica: cada mensagem carrega um identificador próprio, e o servidor reconhece a repetição.
Ao abrir uma conversa, a linha "Recebemos sua mensagem. Respondemos por aqui." fica abaixo da
primeira mensagem até a equipe responder — é o que diz ao visitante que chegou, sem aviso que some.
O texto é a chave chatReceived em data-translations.
Abrir uma conversa nova exige rede: o botão de enviar fica desabilitado enquanto o navegador se declara offline. A fila cobre as respostas dentro de uma conversa que já existe.
Avaliação do atendimento
Quando a conversa é marcada como resolvida ou encerrada, o visitante recebe a pergunta de satisfação
com três opções. A nota é registrada no clique — não há botão de confirmar, e fechar o painel em
seguida não perde nada. É nesse momento que pumahelp:rated dispara e que a nota passa a aparecer
nos relatórios de satisfação do painel.
Logo depois, o cartão agradece e oferece um campo de comentário. Escrever é opcional: quem não quiser, já terminou.
Quem já avaliou não é perguntado de novo — a avaliação existente volta junto da conversa e o widget
mostra a nota dada, com a opção "Trocar avaliação" para corrigir um clique errado (vale sempre a
última nota; nos relatórios ela continua contada na data da primeira avaliação). A avaliação fica
disponível por 30 dias após a última resolução; a API informa o prazo em rateable_until e o
widget esconde os botões depois dele. Uma nota dada em outro dispositivo aparece assim que a conversa
é recarregada.
Não perguntar
Se o seu produto já mede satisfação por outro caminho — ou se este widget atende um público para quem a pergunta não faz sentido —, desligue:
<script src="https://cdn.pumahelp.com/widget/v3/widget.js" type="module"
data-app="acme"
data-api-key="rk_live_..."
data-collect-rating="false"></script>
E no ar, quando a decisão depender de quem está do outro lado:
pumahelp('update', { collectRating: false });
Desligar esconde o cartão, nada mais: uma avaliação já dada continua valendo e continua nos relatórios do painel.
Não pedir comentário
Por padrão o widget oferece o campo de comentário depois da nota. Com
data-rating-comment="false" ele não aparece: a nota é gravada no clique e o cartão recolhe na
hora. Use quando o número lhe basta e você não quer prender a atenção do visitante.
Em qualquer um dos dois casos, quem se enganar tem a opção "Trocar avaliação" enquanto o prazo estiver aberto.
Trocar a aparência e os textos
O cartão é estilizável de fora pelas partes rating, rating-option, rating-option-selected,
rating-comment e rating-submit — veja Partes:
pumahelp-widget::part(rating) { border-radius: 4px; border-color: #cbd5e1; }
pumahelp-widget::part(rating-option) { font-size: 22px; padding: 12px 6px; }
pumahelp-widget::part(rating-option-selected) { background: #0f172a; color: #fff; }
pumahelp-widget::part(rating-submit) { border-radius: 999px; }
A pergunta e os rótulos são textos como quaisquer outros — inclusive emoji, que é o que combina com as opções largas do exemplo acima:
pumahelp('init', {
appName: 'acme',
apiKey: 'rk_live_...',
translations: {
ratingQuestion: 'A gente resolveu o seu problema?',
ratingGood: '😃',
ratingNeutral: '😐',
ratingBad: '😞',
ratingCommentPlaceholder: 'Conta pra gente o que aconteceu',
ratingSubmit: 'Enviar',
},
});
A lista completa das chaves está em Textos e idiomas.
Conversa encerrada
Uma conversa encerrada é definitiva: o widget mostra "Esta conversa foi encerrada." no lugar do
campo de mensagem e o botão "Continuar em uma nova conversa". O que o visitante escrever a partir
daí abre uma conversa nova, ligada à encerrada (a API responde 201 com ela, e o painel mostra
"Acompanhamento de #N"); o widget navega para a conversa nova e anuncia "Nova conversa criada".
Uma conversa resolvida funciona de outro modo: a resposta do visitante a reabre.
Na lista, "Encerrada" aparece em cinza e "Resolvida" em verde.
Aviso sonoro
Dois sons curtos: um marca a mensagem enviada, outro a resposta recebida — este só toca quando a conversa não está na frente da pessoa. Cada um tem o seu interruptor.
Só o de envio desligado:
<script type="module" src="…/widget.js" data-app="acme" data-api-key="rk_live_..."
data-send-sound="false"></script>
Só o de recebimento desligado:
<script type="module" src="…/widget.js" data-app="acme" data-api-key="rk_live_..."
data-receive-sound="false"></script>
Silêncio total:
<script type="module" src="…/widget.js" data-app="acme" data-api-key="rk_live_..."
data-sound="false"></script>
Em init(), são soundEnabled, sendSoundEnabled e receiveSoundEnabled.
Marca
No pé do painel há um link discreto "Criado com PumaHelp", traduzido junto com o resto dos textos (em inglês, "Powered by PumaHelp"). Para escondê-lo:
<script type="module" src="…/widget.js" data-app="acme" data-api-key="rk_live_..."
data-branding="false"></script>
Em init(), é branding: false. O texto pode ser trocado por data-translations com a chave
poweredBy, e o estilo ajustado por ::part(brand) — veja Partes.
Acessibilidade
O painel é um diálogo de verdade: Esc fecha, o foco fica preso enquanto está aberto e volta para o
botão ao sair. Mensagens novas e erros são anunciados por região viva para leitores de tela.
Ao abrir o painel ou entrar numa conversa, o foco vai para o campo de mensagem — no celular ele fica
no título, para o teclado não cobrir a conversa. Se preferir que o foco nunca vá a um campo por conta
própria, data-auto-focus="false" (ou autoFocus: false no init() e no update) o mantém no
título.
📧 E-mail e continuidade entre dispositivos
Depois que a conversa já existe — nunca antes — o widget pode oferecer ao visitante acompanhar por e-mail: um cartão discreto dentro da conversa, logo abaixo da primeira mensagem dele. Quem o dispensa com Agora não só volta a vê-lo depois de uma semana. Quem informa o endereço passa à etapa do código, que continua na tela mesmo se a página for recarregada, pelos 10 minutos em que o código vale — quando a sessão fica guardada no navegador, que é o padrão.
O convite vem desligado. Ligue com data-collect-email="true" na tag, collectEmail: true no
init() — ou só quando fizer sentido, com update. O caso típico: seu site identifica quem está
logado pelo provedor de token, e só oferece o e-mail a quem ficou anônimo:
pumahelp('auth', async () => {
const token = await meuBackend.tokenDoSuporte(); // null quando ninguém está logado
if (!token) pumahelp('update', { collectEmail: true });
return token;
});
// E se a identidade expirar no meio da conversa:
pumahelp('on', 'pumahelp:session-lost', () => pumahelp('update', { collectEmail: true }));
Informado o endereço, o PumaHelp envia um código de 6 dígitos, digitado ali mesmo, no widget em que o e-mail foi informado. Nada é gravado antes disso. O e-mail não traz link: abri-lo no celular não muda nada no computador.
- o código vale por 10 minutos, e uma vez só;
- Reenviar código manda outro a cada 60 segundos, e o novo cancela o anterior;
- Usar outro e-mail volta ao endereço, já preenchido, para corrigir um erro de digitação;
- confirmar numa aba vale para as outras abas do mesmo site, no mesmo navegador — quando a sessão fica guardada no navegador, que é o padrão.
Confirmado, duas coisas passam a valer:
- a equipe vê o e-mail no ticket, marcado como verificado;
- a pessoa pode continuar a conversa em outro dispositivo: no convite de lá, ela informa o mesmo e-mail e digita o código que chegar, e as conversas dos dois aparelhos se juntam.
Segurança do código
- Só vale onde foi pedido. O código confirma o endereço no widget em que ele foi informado, no mesmo navegador. Digitado em outro navegador ou aparelho, é recusado. É por isso que ler o e-mail no celular e digitar o código no computador funciona.
- Tentativas limitadas. Cada código aceita 5 tentativas; depois disso, é preciso pedir outro. Cada endereço recebe no máximo 5 códigos por hora e aceita 10 tentativas por dia, somando todos os visitantes, e cada visitante pede até 10 códigos por dia. Passado um limite, o widget pede para tentar mais tarde.
- O e-mail avisa. A mensagem pede para não compartilhar o código e diz que a equipe de atendimento nunca vai pedi-lo — garanta que a sua equipe, de fato, nunca peça. O código não vai no assunto, que aparece nas notificações da tela bloqueada.
- O dono de um endereço não é revelado. Pedir o código tem a mesma resposta para qualquer endereço, e informar o e-mail de outra pessoa não dá acesso a nada: o código vai para a caixa dela, não para a tela de quem digitou. Os limites de um endereço valem para todos os visitantes juntos, então pedidos em excesso para ele podem atrasar em até um dia a confirmação da própria dona.
📘 TypeScript
Os tipos são publicados ao lado do bundle. Aponte para eles uma vez:
/// <reference types="https://cdn.pumahelp.com/widget/v3/pumahelp-widget.d.ts" />
Ou baixe o pumahelp-widget.d.ts e inclua no include do seu tsconfig.json.
Em React, inclua também o pumahelp-widget-react.d.ts, publicado ao lado — é ele que declara a
tag para o JSX.
A partir daí, window.pumahelp é tipado — inclusive o detalhe de cada evento:
pumahelp('on', 'pumahelp:ticket-created', ({ ticketId, subject }) => {
// ^ number ^ string
console.log(ticketId, subject);
});
Com o arquivo de React incluído, usar a tag direto num componente não acusa erro de elemento desconhecido:
<pumahelp-widget data-app="acme" data-api-key="rk_live_..." />
init() também é tipado: ele devolve o elemento, com openPanel, closePanel, toggle, isOpen,
getUnread, identify, logout, destroy e instanceId. É o que permite dirigir uma segunda
instância sem as.
Não copie definições de tipo de documentação para dentro do seu projeto — uma cópia envelhece em silêncio. Referencie o arquivo publicado, que acompanha a versão do widget que você está usando.
🌐 Integração com frameworks
Na maioria dos casos você não precisa de nada disso: a tag de script no index.html resolve, e o
widget é indiferente a qual framework desenha o resto da página. Os exemplos abaixo servem para
quando o widget precisa reagir ao ciclo de vida da aplicação.
React
import { useEffect } from 'react';
export function Suporte({ usuario }: { usuario?: { token: string } }) {
useEffect(() => {
if (!usuario) return;
// `identify` vale por carregamento de página: este efeito roda de novo a cada montagem, que é
// exatamente o que se quer. Para não depender disso, registre um provedor de token.
window.pumahelp('identify', { token: usuario.token });
}, [usuario]);
useEffect(() => {
const parar = window.pumahelp('on', 'pumahelp:session-lost', async () => {
const { token } = await fetch('/api/suporte/token').then((r) => r.json());
window.pumahelp('identify', { token });
});
return () => parar?.();
}, []);
useEffect(() => {
// `on` devolve a função que cancela a inscrição — é ela que vai no cleanup.
const parar = window.pumahelp('on', 'pumahelp:ticket-created', ({ ticketId }) => {
console.log('conversa aberta', ticketId);
});
return () => parar?.();
}, []);
return null;
}
Vue 3
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
let parar: (() => void) | undefined;
onMounted(() => {
parar = window.pumahelp('on', 'pumahelp:ticket-created', ({ ticketId }) => {
console.log('conversa aberta', ticketId);
});
});
onUnmounted(() => parar?.());
// O template não alcança `window`: expressões Vue só enxergam o que o `setup` expõe, mais uma
// lista fixa de globais que não inclui `window` nem `pumahelp`.
const abrir = () => window.pumahelp('open');
</script>
<template>
<button @click="abrir">Falar com o suporte</button>
</template>
Angular
import { Component, OnDestroy, OnInit } from '@angular/core';
@Component({
selector: 'app-suporte',
template: `<button (click)="abrir()">Falar com o suporte</button>`,
})
export class SuporteComponent implements OnInit, OnDestroy {
private parar?: () => void;
ngOnInit(): void {
this.parar = window.pumahelp('on', 'pumahelp:ticket-created', ({ ticketId }) => {
console.log('conversa aberta', ticketId);
});
}
ngOnDestroy(): void {
this.parar?.();
}
abrir(): void {
window.pumahelp('open');
}
}
Se preferir colocar a tag <pumahelp-widget> direto num template Angular, o componente precisa de
schemas: [CUSTOM_ELEMENTS_SCHEMA] — sem isso o Angular acusa NG0304. Usar só a tag de script, ou
o init(), evita o assunto.
Aplicações de página única
O widget não é remontado a cada troca de rota e não precisa ser. Se você realmente quiser removê-lo de uma área do site:
pumahelp('destroy');
🔁 Migrando da v2
Cada versão é uma release própria no CDN: a v2.1.1 continua publicada em
https://cdn.pumahelp.com/widget/v2.1.1/widget.js e continua atendida pela API, e a v3 está em
https://cdn.pumahelp.com/widget/v3/widget.js. Nada muda no seu site enquanto você não trocar a URL
do script — a migração é exatamente essa troca, mais os ajustes da tabela abaixo.
| v2 | v3 |
|---|---|
https://cdn.pumahelp.com/widget/v2.1.1/widget.js | https://cdn.pumahelp.com/widget/v3/widget.js |
<puma-help-widget> | <pumahelp-widget> — ou nenhuma tag, só o script |
window.PumaHelp.init({...}) | pumahelp('init', {...}), e funciona antes de carregar |
app-name / api-key | data-app / data-api-key |
PumaHelp.identify({ accessToken }) | pumahelp('identify', { token }) — accessToken ainda é aceito |
| Identidade sobrevivia ao F5 | Vale por carregamento; use um provedor de token para durar |
Evento auth-error na queda da sessão | pumahelp:session-lost |
identify({ name }) alimentava o nome do convidado | O nome vem do servidor; name não é mais usado |
widget.on('ready', fn) | pumahelp('on', 'pumahelp:ready', fn) |
Evento error, sem prefixo | pumahelp:error |
event.detail.payload | Detalhe tipado por evento — veja a tabela de eventos |
debug | Não existe. Erros saem por pumahelp:error |
theme, language, color | data-theme, data-language, data-color |
| Sem tipos publicados | .d.ts no CDN |
| Chave de API obrigatória | pumahelp('auth', fn) dispensa a chave |
| Sem avaliação | Avaliação ao resolver a conversa |
Três diferenças que costumam pegar quem migra:
- Booleano é booleano. Na v3,
"false","0","no","off"e vazio desligam; qualquer outro valor liga. Confira os atributos booleanos ao migrar. embeddedembute de verdade. Na v3, embutido é embutido: sem botão flutuante, ocupando o container. Se você usavaembeddedna v2, confira o resultado na tela.- Eventos têm prefixo e detalhe tipado. Se o seu código lia
event.detail.ticketIdna v2, ele liaundefined: o detalhe vinha emevent.detail.payload. Na v3,ticketIdestá onde a tabela de eventos diz que está. - A identidade não sobrevive mais ao recarregamento sozinha. A v3 não guarda o token de acesso,
então quem usa só
identify()precisa chamá-lo a cada carregamento. O provedor de token resolve isso de vez, e é o caminho recomendado.
❓ Perguntas frequentes
A chave de API no HTML não é um risco? Ela é pública, e é por isso que precisa ser restrita ao escopo de criar convidado. Com esse escopo, o que um visitante mal-intencionado consegue fazer com ela é abrir uma conversa — que é exatamente o que o widget faz. Se você tem backend e prefere não expor nada, use o provedor de token.
O widget conflita com o CSS ou o JavaScript do meu site? Não. Ele roda dentro de um Shadow DOM: o seu CSS não entra, o dele não vaza. Os eventos são todos prefixados justamente para não colidir com os seus.
Preciso avisar sobre cookies?
O widget não usa cookies. No armazenamento local do navegador ele guarda a credencial de sessão e,
se o visitante dispensar o convite de e-mail, a data em que o fez. Com o convite de
e-mail ligado, guarda também o endereço informado
enquanto o visitante está na etapa do código: ele sai ao confirmar, ao trocar de endereço ou quando o
código vence — e, se a página for fechada antes, na próxima vez que o widget carregar no seu site.
Com data-persist-session="false", a credencial e o endereço ficam só em memória e somem ao fechar a
aba; só a data da dispensa continua guardada.
O visitante perde o histórico se limpar o navegador?
Perde, se for anônimo — é o que "anônimo" significa. Com identify() ou com o provedor de token, o
histórico está atrelado à identidade do seu site e volta em qualquer dispositivo. Com e-mail
confirmado, basta informar o mesmo e-mail no widget e digitar o código: o histórico volta.
Dá para abrir o widget a partir de um botão meu?
Sim: pumahelp('open'). Funciona mesmo antes de o widget carregar.
Dá para ter mais de um widget na mesma página?
Sim, mas os comandos de window.pumahelp agem sempre sobre a primeira instância. Para dirigir a
segunda, guarde o que init() devolve e chame os métodos do próprio elemento:
const suporte = pumahelp('init', { appName: 'acme', apiKey: 'rk_live_...' });
const vendas = pumahelp('init', { appName: 'acme', apiKey: 'rk_live_...', container: caixa });
vendas.openPanel(); // pumahelp('open') abriria o primeiro
Em TypeScript funciona direto: init() é tipado e devolve o elemento com todos esses métodos.
O widget funciona sem JavaScript ou em navegador antigo? Não. Ele é um módulo ES e depende de Custom Elements e Shadow DOM — o que qualquer navegador com atualização automática tem desde 2018.