Pular para o conteúdo principal

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.

important

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.

ModoComo se ativaQuando usar
Chave restritadata-api-keySite sem backend, ou visitante anônimo
Provedor de tokenpumahelp('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.

aviso

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.

Este é o modo durável

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ágina

O 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().

AtributoPropriedadePadrãoO que faz
data-appappName—Obrigatório. Subdomínio da organização. Aceita letras, números e hífen
data-api-keyapiKey—Chave restrita. Dispensável quando você registra um provedor de token
data-themethemeautolight, dark ou auto (segue o sistema do visitante)
data-languagelanguagept-BRIdioma dos textos e das datas
data-colorcolor#f97116Cor 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-iconiconpumaÍ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-embeddedembeddedfalseEmbute no lugar de flutuar
data-z-indexzIndex2147483000Camada do widget
data-soundsoundEnabledtrueGeral: false silencia os dois sons abaixo
data-send-soundsendSoundEnabledtrueSom ao enviar uma mensagem ou abrir uma conversa
data-receive-soundreceiveSoundEnabledtrueSom ao receber uma resposta
data-persist-sessionpersistSessiontruefalse mantém a sessão só em memória
data-brandingbrandingtrueLink "Criado com PumaHelp" no pé do painel. false esconde
data-collect-emailcollectEmailfalseConvida o visitante anônimo a acompanhar a conversa por e-mail. Veja E-mail
data-collect-ratingcollectRatingtruePergunta a satisfação quando a conversa é resolvida ou encerrada. false não pergunta. Veja Avaliação
data-rating-commentratingCommenttrueOferece um campo de comentário depois da nota. false recolhe o cartão assim que a nota é dada
data-auto-focusautoFocustrueLeva 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-labellauncherLabel—Texto ao lado do ícone do botão flutuante; o botão vira uma pílula. Veja Texto e tamanho do botão
data-translationstranslations—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-tagstags—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_...' });
ComandoArgumentosDevolve
initConfigA instância criada
auth() => string | null | Promise<string | null>—
open / close / toggle——
identify{ token }Promise<void> — ou nada, quando enfileirado
logout——
updateobjeto com opções—
destroy——
onevento, ouvinteFunçã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.

Eventoevent.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.

Só na abertura

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' });

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ção

Ela é 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ávelPadrã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-strongdestaque 78% com pretodestaque 85% com brancoAnel de foco, borda do botão flutuante, seta de enviar acesa
--pw-accent-tintdestaque a 14% sobre brancodestaque a 22% sobre o painelÁreas grandes: balão do visitante e nota escolhida
--pw-bg#ffffff#1c1a19Fundo do painel
--pw-bg-subtle#faf8f6#262321Fundos secundários: cabeçalho, campos, cartões
--pw-fg#1c1917#ece8e4Texto principal
--pw-fg-muted#78716c#a8a29eTexto secundário: datas, legendas
--pw-border#eae5e1#3a3532Bordas e divisórias
--pw-bubble-agent#f4f1ee#2b2725Balã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#f87171Erros e ações destrutivas
--pw-success#16a34a#4ade80Confirmações
--pw-radius16px—Arredondamento
--pw-shadowsombra suavemais densaSombra do painel e do botão
--pw-badge#ef4444—Contador de não lidas sobre o botão
--pw-launcher-size56px—Tamanho do botão flutuante; ícone e contador acompanham
--pw-fontpilha do sistema—Família tipográfica — só no seletor do elemento, não em ::part(root)
--pw-z2147483000—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; }
ParteO que é
rootRaiz do widget. É onde ficam as variáveis de tema
launcherBotão flutuante
badgeContador de não lidas sobre o botão
launcher-labelTexto ao lado do ícone do botão flutuante. Vazio sem data-launcher-label
launcher-imageA sua logo no botão flutuante, quando data-icon é uma URL
surfacePainel da conversa
headerCabeçalho do painel
bodyÁrea rolável das mensagens
footerRodapé do painel
composerO formulário de escrever — a área toda, não o campo de texto, que não tem parte própria
sendBotão de enviar
brandLink "Criado com PumaHelp" no pé do painel
ratingCartão de avaliação. Vale tanto para a pergunta quanto para o "Você avaliou: …"
rating-optionCada uma das três opções de nota
rating-option-selectedA opção escolhida. Vem junto de rating-option, nunca sozinha
rating-commentCampo de comentário da avaliação
rating-submitBotão de enviar a avaliação
observaçã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:

GrupoChaves
Geraltitle, close, back, retry, offline
ListalistError, listLoadMore, listClosed, newConversation, previewYou
Nova conversawelcome, welcomeHint, newPlaceholder, newError
ConversachatError, chatPlaceholder, chatPlaceholderOffline, chatSend, chatTyping, chatReceived
MensagemmessageSending, messageFailed, discard, unreadOne, unreadMany
Anexosattach, attachments, attachmentsPending, attachmentUploading, attachmentRemove, attachmentsTooMany, attachmentError
AvaliaçãoratingQuestion, ratingGood, ratingNeutral, ratingBad, ratingCommentPlaceholder, ratingSubmit, ratingThanks, ratingGiven, ratingChange, ratingCancel, ratingError
E-mailemailInvite, emailInviteHint, emailPlaceholder, emailSubmit, emailDismiss, emailSent, emailCodeLabel, emailCodeSubmit, emailResend, emailResendIn, emailCodeInvalid, emailCodeExpired, emailTooMany, emailUnavailable, emailChange, emailError, emailConfirmed
SituaçãostatusSolved, statusClosed
Notificação do navegadornotificationTitle, notificationBody
Botão flutuantelauncherOpen, 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.

A conexão existe enquanto o painel existir

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.

A fila vale para responder, não para abrir

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.

important

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');
}
}
observação

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.

v2v3
https://cdn.pumahelp.com/widget/v2.1.1/widget.jshttps://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-keydata-app / data-api-key
PumaHelp.identify({ accessToken })pumahelp('identify', { token }) — accessToken ainda é aceito
Identidade sobrevivia ao F5Vale por carregamento; use um provedor de token para durar
Evento auth-error na queda da sessãopumahelp:session-lost
identify({ name }) alimentava o nome do convidadoO nome vem do servidor; name não é mais usado
widget.on('ready', fn)pumahelp('on', 'pumahelp:ready', fn)
Evento error, sem prefixopumahelp:error
event.detail.payloadDetalhe tipado por evento — veja a tabela de eventos
debugNão existe. Erros saem por pumahelp:error
theme, language, colordata-theme, data-language, data-color
Sem tipos publicados.d.ts no CDN
Chave de API obrigatóriapumahelp('auth', fn) dispensa a chave
Sem avaliaçãoAvaliação ao resolver a conversa

Três diferenças que costumam pegar quem migra:

  1. Booleano é booleano. Na v3, "false", "0", "no", "off" e vazio desligam; qualquer outro valor liga. Confira os atributos booleanos ao migrar.
  2. embedded embute de verdade. Na v3, embutido é embutido: sem botão flutuante, ocupando o container. Se você usava embedded na v2, confira o resultado na tela.
  3. Eventos têm prefixo e detalhe tipado. Se o seu código lia event.detail.ticketId na v2, ele lia undefined: o detalhe vinha em event.detail.payload. Na v3, ticketId está onde a tabela de eventos diz que está.
  4. 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.