Integrações e API
Guia técnico do Widget Contact Desk para integração com Spincare
Documentação completa para implantar o Chat Widget do Contact Desk em sistemas web, WebView, Android, iOS ou interfaces nativas integradas ao Spincare.
Resposta rápida
O Chat Widget do Contact Desk pode ser integrado de duas formas: usando o script oficial, quando a interface do Contact Desk será exibida dentro de uma página ou WebView; ou usando a API pública do widget, quando o Spincare quiser construir uma experiência própria no aplicativo Android/iOS. Em ambos os cenários, a integração usa uma widget key pública, valida origem autorizada, mantém credenciais administrativas fora do front-end e registra a conversa no Contact Desk para operação, auditoria, chatbot, atendimento humano, anexos, CSAT e criação de tickets.
Passo a passo
- Defina com o Contact Desk qual tenant, fila, categoria e chatbot serão usados no canal Spincare.
- Cadastre os domínios, hosts ou origens autorizadas nas configurações do widget.
- Escolha o modo de integração: script oficial para WebView ou API pública para interface nativa.
- Consulte o endpoint de configuração antes de iniciar a conversa.
- Crie ou recupere a conversa com um session_id persistente e exclusivo por usuário/dispositivo/contexto.
- Envie mensagens com client_message_id para evitar duplicidade em retries.
- Sincronize mensagens por SSE ou por consulta incremental usando after_id.
- Envie anexos somente quando allow_attachments estiver habilitado.
- Registre feedback CSAT quando habilitado e encerre a conversa ao finalizar o atendimento.
Objetivo desta documentação
Este guia foi escrito para equipes técnicas que vão substituir outro widget de atendimento pelo Contact Desk, especialmente em cenários onde o widget será embarcado dentro de um produto de terceiros, como o aplicativo Android/iOS do Spincare. A documentação descreve fluxo, responsabilidades, payloads, limites, segurança e roteiro de homologação.
- Explicar o funcionamento do widget sem depender de acesso administrativo ao Contact Desk.
- Diferenciar instalação por script, WebView e consumo direto da API pública.
- Definir quais dados podem trafegar no navegador ou aplicativo.
- Reduzir risco de mensagens duplicadas, perda de contexto ou vazamento de credenciais.
Quando usar script oficial, WebView ou API pública
A escolha depende do nível de controle visual e operacional desejado. O script oficial é mais rápido e mantém a experiência visual do Contact Desk. A API pública exige mais desenvolvimento do integrador, mas permite criar uma interface totalmente nativa dentro do aplicativo.
- Script oficial: recomendado para sites e portais web onde o balão do Contact Desk pode aparecer no canto da tela.
- WebView: recomendado quando o aplicativo móvel pode carregar uma página web controlada e autorizada.
- API pública: recomendado quando o Spincare deseja construir a tela de chat nativa, usando componentes próprios de Android/iOS.
- Não misture script e API nativa na mesma sessão sem alinhar a estratégia de session_id.
Pré-requisitos de configuração no Contact Desk
Antes da integração, um administrador do tenant deve publicar o widget e validar o canal de atendimento. O Spincare não precisa receber senha, token administrativo, chave SMTP, chave de banco, segredo de webhook ou credencial interna.
- Widget ativo e publicado no tenant correto.
- Widget key pública fornecida pelo administrador.
- Fila padrão definida para receber as conversas.
- Categoria padrão definida quando conversas virarem ticket.
- Chatbot publicado, se o atendimento inicial for automatizado.
- Domínios/origens autorizadas cadastradas.
- Anexos e CSAT habilitados somente se fizerem parte do fluxo.
Dados que o Spincare deve receber
A integração deve receber somente dados públicos e operacionais. A widget key identifica o widget, mas não substitui a validação de origem e não concede acesso administrativo ao Contact Desk.
- Ambiente de produção: https://app.contactdesk.com.br
- Endpoint base: https://app.contactdesk.com.br/api/widget/{WIDGET_KEY}
- Widget key pública do tenant.
- Lista de origens/domínios autorizados.
- Campos obrigatórios retornados pelo GET /config.
- Limites de anexo e formato aceitos.
- Contato técnico para homologação e suporte.
Estratégia de session_id
O session_id é o identificador público da conversa no contexto do widget. Ele precisa ser persistente para recuperar uma conversa em andamento, mas não deve ser compartilhado entre pessoas diferentes.
- Use um session_id único por usuário, dispositivo e contexto de atendimento.
- Em aplicativo nativo, armazene o session_id em storage seguro do app enquanto a conversa estiver ativa.
- Em WebView, evite depender apenas de localStorage se o app limpar dados entre sessões.
- Não use CPF, e-mail puro, ID interno de paciente ou número de prontuário como session_id.
- Se precisar relacionar profissional ou paciente, envie contexto apenas após acordo técnico e com dados mínimos necessários.
Fluxo recomendado da conversa
O fluxo seguro começa com a consulta da configuração, passa pela criação ou retomada da conversa, envia mensagens com idempotência e acompanha respostas por stream ou consulta incremental.
- 1. Consultar GET /config para saber disponibilidade, aparência, campos obrigatórios, anexos e CSAT.
- 2. Criar ou retomar POST /conversation com session_id e dados exigidos.
- 3. Enviar mensagem em POST /message com client_message_id único.
- 4. Receber respostas por GET /stream ou GET /messages?after_id={ultimo_id}.
- 5. Enviar anexos por POST /attachment quando permitido.
- 6. Encerrar por POST /end quando o usuário finalizar.
- 7. Enviar POST /feedback quando CSAT estiver habilitado.
Chatbot, atendimento humano e handoff
Quando um chatbot está associado ao widget, as mensagens iniciais, menus e opções exibidas ao visitante são controladas pelo fluxo publicado no módulo Chatbot. As mensagens configuradas na aba Mensagens do widget ficam preservadas para fallback humano, mas não duplicam o conteúdo do chatbot.
- O chatbot pode iniciar a conversa, apresentar menus e direcionar para fila humana.
- O atendimento humano assume quando o fluxo encaminha para operador ou fila.
- A interface deve exibir claramente se o usuário está com bot ou humano.
- Eventos de digitação podem ser exibidos para tornar a conversa mais natural.
- O bot pode ter delay por bloco para reduzir respostas instantâneas artificiais.
Mensagens, paginação e atualização incremental
Conversas longas não devem carregar todo o histórico a cada atualização. A API suporta leitura incremental e paginação para reduzir tráfego, latência e carga no banco de dados.
- Use after_id para buscar somente mensagens novas.
- Use before_id para carregar mensagens antigas sob demanda.
- Use limit entre 1 e 100 registros.
- Guarde o last_id retornado pela API para a próxima sincronização.
- Evite polling agressivo quando o stream SSE estiver ativo.
Idempotência no envio de mensagens
Aplicativos móveis podem reenviar uma requisição por instabilidade de rede, duplo clique ou retry automático. Para evitar mensagens duplicadas, envie client_message_id único em cada mensagem do usuário.
- Gere um client_message_id por mensagem antes de chamar a API.
- Reutilize o mesmo client_message_id em tentativas de retry da mesma mensagem.
- Se a API identificar duplicidade, ela retorna duplicate=true com a mensagem já registrada.
- Não reutilize o mesmo client_message_id para mensagens diferentes.
Anexos e imagens coladas
O widget permite anexos quando a configuração allow_attachments estiver habilitada. No script oficial, imagens coladas na caixa de mensagem podem ser tratadas como arquivo enviado pelo chat. Em integração nativa, o aplicativo deve enviar o arquivo pelo endpoint de attachment.
- Tamanho padrão do widget público: até 10 MB por arquivo.
- Tipos comuns aceitos: JPG, PNG, WEBP, GIF, PDF, TXT, CSV, ZIP, DOC, DOCX, XLS e XLSX.
- O upload deve usar multipart/form-data.
- Downloads devem usar a URL assinada retornada pela API ou pela mensagem serializada.
- Não construa manualmente URLs de download.
Rate limit e simultaneidade
As rotas públicas possuem limitação por rota, widget, session_id e IP. Isso evita abuso sem bloquear indevidamente todos os usuários de uma mesma rede corporativa.
- GET /config: até 180 chamadas por minuto por chave de controle.
- POST /conversation: até 30 chamadas por minuto.
- POST /message: até 45 mensagens por minuto.
- POST /attachment: até 12 uploads por minuto.
- GET /messages: até 180 chamadas por minuto.
- GET /stream: até 40 streams por minuto.
- POST /end e POST /feedback: até 30 chamadas por minuto.
Eventos e observabilidade
Para integração crítica, os times devem conseguir investigar falhas sem acessar dados sensíveis desnecessários. O Contact Desk registra eventos operacionais do widget com hashes de identificadores e contexto técnico suficiente para diagnóstico.
- Origem negada.
- Conversa iniciada ou retomada.
- Mensagem recebida.
- Mensagem duplicada por client_message_id.
- Anexo enviado.
- Falha de chatbot ou transferência.
- Rate limit aplicado.
- Conversa encerrada e CSAT recebido.
Recomendação específica para Android e iOS do Spincare
Se o objetivo for substituir o widget atual rapidamente, a WebView com script oficial reduz o prazo de implantação. Se o objetivo for uma experiência totalmente integrada ao aplicativo, a API pública é o caminho mais limpo, desde que o Spincare implemente a interface e siga o contrato de sessão, idempotência e paginação.
- Para primeira versão: WebView ou página web autorizada pode acelerar a homologação.
- Para experiência definitiva: construir UI nativa consumindo a API pública.
- Nunca embutir token administrativo do Contact Desk no aplicativo.
- Evitar armazenar dados sensíveis do paciente no histórico local do app.
- Usar logs com correlation_id interno do Spincare para cruzar chamados sem expor dados pessoais.
Checklist de homologação antes de publicar
Antes de liberar para usuários reais, valide o fluxo completo em Android, iOS, navegador, rede corporativa, 4G/5G e cenários de instabilidade.
- Widget carrega apenas em origem autorizada.
- Origem não autorizada retorna 403.
- Conversa inicia com campos obrigatórios corretos.
- Chatbot responde o fluxo publicado.
- Handoff para humano funciona.
- Indicador de digitação aparece sem travar a conversa.
- Mensagem com retry não duplica.
- Busca incremental por after_id funciona.
- Upload de imagem, PDF e documento funciona.
- Download de anexo usa URL assinada.
- CSAT aparece somente quando habilitado.
- Encerramento muda status corretamente.
- Logs permitem rastrear falhas sem expor credenciais.
Endpoints documentados
| Método | Rota | Autenticação | Uso |
|---|---|---|---|
| GET | /api/widget/{widgetKey}/config |
Widget key pública + origem autorizada | Retorna status, aparência, chatbot, anexos, CSAT, departamentos e campos obrigatórios. |
| POST | /api/widget/{widgetKey}/conversation |
Widget key pública + origem autorizada | Inicia ou recupera conversa ativa usando session_id. |
| POST | /api/widget/{widgetKey}/conversation/{conversationId}/message |
session_id da conversa | Envia mensagem do visitante com client_message_id opcional para idempotência. |
| GET | /api/widget/{widgetKey}/conversation/{conversationId}/messages?session_id={sessionId}&after_id={lastId}&limit=80 |
session_id da conversa | Lista mensagens com paginação incremental; suporta after_id, before_id e limit. |
| GET | /api/widget/{widgetKey}/conversation/{conversationId}/stream?session_id={sessionId}&last_id={lastId} |
session_id da conversa | Abre stream SSE para receber mensagens e estados em tempo quase real. |
| POST | /api/widget/{widgetKey}/conversation/{conversationId}/attachment |
session_id da conversa | Envia anexo via multipart/form-data quando anexos estiverem habilitados. |
| GET | /api/widget/{widgetKey}/conversation/{conversationId}/attachments/{messageId} |
URL assinada temporária | Baixa anexo privado da conversa usando assinatura válida. |
| POST | /api/widget/{widgetKey}/conversation/{conversationId}/end |
session_id da conversa | Encerra conversa ativa ou em espera. |
| POST | /api/widget/{widgetKey}/conversation/{conversationId}/feedback |
session_id da conversa | Registra avaliação CSAT quando habilitada no widget. |
Exemplos seguros
<!-- Contact Desk Chat Widget -->
<script>
(function(w,d,s,o,f,js,fjs){
w['ContactDeskWidget']=o;
w[o]=w[o]||function(){(w[o].q=w[o].q||[]).push(arguments)};
js=d.createElement(s);
fjs=d.getElementsByTagName(s)[0];
js.id=o;
js.src=f;
js.async=1;
fjs.parentNode.insertBefore(js,fjs);
}(window,document,'script','cdw','https://app.contactdesk.com.br/widget/widget.js'));
cdw('init', '{WIDGET_KEY_PUBLICA}');
</script>
<!-- End Contact Desk Chat Widget -->
const widgetKey = '{WIDGET_KEY_PUBLICA}';
const baseUrl = `https://app.contactdesk.com.br/api/widget/${widgetKey}`;
const response = await fetch(`${baseUrl}/config`, {
headers: { Accept: 'application/json' },
});
if (!response.ok) {
throw new Error(`Falha ao carregar widget: ${response.status}`);
}
const config = await response.json();
const requiresName = Boolean(config.requirements?.name);
const requiresEmail = Boolean(config.requirements?.email);
const allowAttachments = Boolean(config.allow_attachments);
const sessionId = await getOrCreateSecureSessionId();
const response = await fetch(`${baseUrl}/conversation`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
session_id: sessionId,
visitor_name: 'Nome do visitante',
visitor_email: 'visitante@empresa.com',
visitor_phone: '(00) 00000-0000',
initial_message: 'Olá, preciso de atendimento.',
}),
});
const data = await response.json();
const conversationId = data.conversation.id;
const lastId = data.last_id ?? 0;
const clientMessageId = crypto.randomUUID();
const response = await fetch(`${baseUrl}/conversation/${conversationId}/message`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
session_id: sessionId,
sender_name: 'Visitante',
message: 'Preciso acompanhar minha solicitação.',
client_message_id: clientMessageId,
}),
});
const data = await response.json();
if (data.duplicate === true) {
renderMessage(data.message);
}
async function fetchNewMessages(lastId) {
const url = new URL(`${baseUrl}/conversation/${conversationId}/messages`);
url.searchParams.set('session_id', sessionId);
url.searchParams.set('after_id', String(lastId));
url.searchParams.set('limit', '80');
const response = await fetch(url, {
headers: { Accept: 'application/json' },
});
const data = await response.json();
return {
messages: data.messages ?? [],
lastId: data.last_id ?? lastId,
typing: data.typing ?? null,
};
}
const formData = new FormData();
formData.append('session_id', sessionId);
formData.append('file', selectedFile);
const response = await fetch(`${baseUrl}/conversation/${conversationId}/attachment`, {
method: 'POST',
body: formData,
});
if (response.status === 403) {
throw new Error('Anexos não estão habilitados para este widget.');
}
const data = await response.json();
renderMessage(data.message);
Cuidados de segurança
- A widget key é pública, mas deve funcionar apenas em origens autorizadas no Contact Desk.
- Nunca envie token administrativo, senha SMTP/IMAP, chave de banco, segredo de webhook ou credencial interna para o aplicativo ou navegador.
- Use HTTPS em todos os ambientes, incluindo páginas WebView.
- Não use CPF, e-mail ou ID de paciente como session_id.
- Evite enviar dados clínicos ou dados sensíveis em metadados livres sem necessidade operacional e base legal.
- Implemente retry com client_message_id para não duplicar mensagens.
- Use after_id e limit para não recarregar conversas longas a cada atualização.
- Baixe anexos apenas por URLs assinadas retornadas pela API.
- Registre logs no Spincare com identificadores internos ou hashes, não com credenciais ou dados sensíveis em texto aberto.
- Valide limites de anexo no aplicativo antes do upload para melhorar experiência do usuário.
Boas práticas
- Revise permissões antes de liberar recursos para novos usuários.
- Teste mudanças em um fluxo controlado antes de aplicar em toda a operação.
- Use nomes claros para filas, categorias, automações e respostas prontas.
- Consulte logs e auditoria quando uma ação precisar de rastreabilidade.
Perguntas frequentes
A widget key é uma senha?
Não. A widget key é um identificador público do widget. A segurança depende da validação de origem, da sessão da conversa, dos limites de uso e das URLs assinadas para anexos. Mesmo assim, ela não deve ser divulgada fora do contexto de instalação.
O Spincare precisa receber token da API do Contact Desk?
Para o widget público, não. O widget usa endpoints próprios com widget key e origem autorizada. Tokens administrativos só são necessários para integrações privadas de backend e não devem ser usados no aplicativo móvel.
O widget substitui o módulo Chatbot?
Não. O widget é o canal de entrada. Quando há chatbot vinculado, o fluxo publicado no módulo Chatbot controla saudação, menus, atrasos e encaminhamento para humano.
É possível criar uma interface nativa no Android/iOS?
Sim. Nesse caso, o app consome a API pública do widget e implementa a interface de conversa. É obrigatório respeitar session_id, client_message_id, paginação incremental, rate limit, anexos e encerramento.
Como o Contact Desk evita mensagens duplicadas?
O endpoint de mensagem aceita client_message_id. Quando o aplicativo reenviar a mesma mensagem por instabilidade, deve reutilizar o mesmo identificador. A API pode retornar duplicate=true quando a mensagem já estiver registrada.
Como anexos são protegidos?
Os arquivos são gravados no armazenamento privado do Contact Desk e o download público usa URL assinada temporária. O integrador não deve montar links manualmente nem expor caminhos internos de storage.
Quando acionar o administrador?
Acione um administrador quando a configuração envolver credenciais, permissões, integrações, filas compartilhadas, políticas de SLA, automações globais ou dados sensíveis.