Widget de Chat — Guia de Implementação (Web e App Mobile)
API pública do widget de chat da plataforma TITOV. É o contrato usado tanto pelo
widget web (widget.js) quanto por um app mobile nativo (React Native, Flutter,
Swift, Kotlin) que queira embutir o chat.Gerado a partir do código (routes/widget.php, WebChatController,
WebChatDomainMiddleware, WebChatDeliveryService) em 2026-07-17.1. Arquitetura: SSE, não WebSocket#
O widget não usa WebSocket. As respostas do atendente e da IA chegam por
SSE (Server-Sent Events): o cliente abre um EventSource em /sse, e o servidor
mantém a conexão por ~30 segundos entregando mensagens de uma fila (Redis); depois
fecha, e o cliente reconecta. O envio de mensagens é HTTP POST comum.Implicação mobile: EventSource não é nativo em React Native nem Flutter — use
uma biblioteca de SSE (ex.: react-native-sse) ou um cliente HTTP que leia o stream
text/event-stream linha a linha. Em Swift/Kotlin, URLSession/OkHttp lendo o
stream resolve.
2. Pré-requisito: cadastrar o App ID#
Um navegador é autorizado pelo header Origin (ou Referer, para same-origin),
validado contra os domínios permitidos do widget. Um app nativo não envia Origin,
então usa outro mecanismo: o header X-Widget-App-Id.No painel, em Configurações → AI Functions → Widgets, edite o widget e preencha
"IDs de apps nativos permitidos" com o bundle identifier do app
(ex.: com.suaempresa.app), um por linha. Só apps cadastrados são aceitos.Sem curinga. Diferente dos domínios, o campo de apps não aceita *. Cada App ID
deve ser declarado explicitamente, e a comparação é exata (in_array estrito) — envie
o valor sem espaços nas bordas.
3. Autenticação#
Dois tokens, ambos públicos no sentido de que ficam no cliente:Widget token (wgt_...): identifica o widget/organização. Vai no path da URL.
Vem do snippet de instalação no painel.
Session token (UUID): identifica a conversa anônima do visitante. Criado via
POST /session, deve ser persistido no dispositivo.
Em toda requisição a partir de um app nativo, envie:X-Widget-App-Id: com.suaempresa.app
A resposta pode conter Access-Control-Allow-Origin: * — é o CORS global da plataforma
e é inofensivo para um app nativo (não há navegador impondo same-origin). A autorização
real é o App ID; requisição sem App ID cadastrado recebe 403.
4. Fluxo típico#
1.
GET /config para montar a UI (opcional).
2.
Se não houver session token salvo, POST /session e persista o token.
3.
GET /messages para carregar o histórico.
4.
Abra o EventSource em /sse para receber respostas.
5.
POST /messages (ou /media) para enviar; mostre a mensagem localmente de imediato.
5. Persistência da sessão#
O widget web guarda o session token no localStorage. No mobile, guarde no armazenamento
do app (AsyncStorage, shared_preferences, UserDefaults) para retomar a conversa entre
aberturas.Trate o 401 como sessão expirada. Se POST /messages ou /media retornar 401,
descarte o token salvo, crie uma nova sessão e reenvie. O GET /messages retorna 410
para sessão expirada (e 401 para token de sessão inválido) — mesmo tratamento.
6. Upload de mídia#
O visitante pode enviar imagem, áudio e documento. Vídeo e SVG não são aceitos.| Tipo | Formatos aceitos | Limite |
|---|
| Imagem | JPEG, PNG, WebP, GIF | 10 MB |
| Áudio | MP3, MP4/M4A, OGG, WAV, WebM, AAC | 10 MB |
| Documento | PDF, TXT, CSV, DOC, DOCX, XLS, XLSX | 20 MB |
A validação é por conteúdo real do arquivo (mimetypes), não pela extensão. A IA
transcreve áudio e lê imagem e documento automaticamente.Além do limite por arquivo há uma quota acumulada de 50 MB por sessão por hora e um
limite de 5 uploads por minuto por sessão; ambos retornam 429.Não defina Content-Type manualmente no upload — deixe o cliente HTTP montar o
multipart/form-data com o boundary correto.
7. Rate limits#
| Rota | Limite | Escopo | Configurável |
|---|
POST /session | 10/hora (padrão) | Por IP | settings.rate_limits.sessions_per_hour |
POST /messages | 20/min (padrão) | Por sessão | settings.rate_limits.messages_per_minute |
POST /media | 5/min + 50 MB/hora | Por sessão | Não |
8. Tratamento de erros#
Todas as rotas JSON respondem {"error": "..."} com o status apropriado.| Status | Significado | Ação no app |
|---|
| 400 | Handoff indisponível (WhatsApp não configurado / número não resolvido) | Ocultar o botão de handoff. |
| 401 | Sessão inválida ou expirada | Criar nova sessão e reenviar. |
| 403 | Widget token inválido/inativo, App ID não cadastrado, ou Origin não permitida | Verificar App ID/domínio no painel. |
| 410 | Sessão expirada (só no GET /messages) | Criar nova sessão. |
| 413 | Arquivo grande demais (limite do servidor web) | Reduzir o arquivo. |
| 422 | Texto vazio, ou tipo/tamanho de arquivo inválido | Mostrar mensagem clara ao usuário. |
| 429 | Rate limit ou quota excedida | Aguardar e repetir. |
| 503 | Limite de contatos da assinatura atingido | Tentar mais tarde. |
O GET /sse é a exceção: sessão inválida não retorna erro HTTP. O stream abre
com 200 e entrega um event: error com {"message":"Invalid session"}. Trate esse
evento, não o status.
9. Limitações conhecidas#
Sem push notifications. A conexão SSE é suspensa pelo SO quando o app sai de primeiro
plano. Com o app fechado, o usuário não recebe mensagens — o caso "o atendente responde
depois" não funciona sem push (FCM/APNs), que não faz parte desta integração.
Limite de sessões por IP. Em redes móveis com CGNAT muitos usuários compartilham o
mesmo IP; se necessário, aumente sessions_per_hour no painel do widget.
Sem vídeo no envio do visitante (imagem, áudio e documento apenas).
Histórico limitado a 50 mensagens no GET /messages.
10. Exemplos#
Criar sessão e enviar texto (fetch — universal)#
Enviar mídia (multipart)#
Receber respostas por SSE#
Em Flutter, use um pacote de SSE (ou http lendo o stream) apontando para a mesma URL
e header. O contrato dos eventos é idêntico. Modificado em 2026-07-17 11:43:01