Webhooks de saída
Receba no seu sistema um aviso HTTP quando algo acontece no atendimento: mensagem recebida, enviada, entregue ou lida; conversa criada, atribuída, transferida ou encerrada; OS aberta ou atualizado. Cada entrega é um POST com corpo JSON, assinado com HMAC SHA-256, e o ChatISP tenta de novo sozinho quando o seu sistema está fora do ar.
Visão geral
Os webhooks são o caminho inverso da API: em vez de o seu sistema perguntar ao ChatISP o que mudou, o ChatISP avisa o seu sistema quando acontece. Usos típicos num provedor:
- Registrar no CRM cada atendimento encerrado, com protocolo e motivo, e buscar o histórico pela API.
- Saber quando o cliente respondeu a um aviso enviado pelo ERP e acionar uma automação.
- Acompanhar se o boleto enviado pelo WhatsApp foi entregue e lido.
- Espelhar ordens de serviço no sistema de ordens de serviço e saber quando mudam de etapa ou de responsável.
- Alimentar um painel próprio com a fila e a distribuição dos atendimentos em tempo real.
Como funciona
- Você cadastra um webhook com o endereço do seu sistema e os eventos de interesse.
- Quando um evento acontece, o ChatISP grava a entrega numa fila. O atendimento nunca espera pelo seu sistema: se ele estiver lento ou fora do ar, nada muda para a equipe nem para o cliente.
- O serviço de segundo plano do ChatISP passa pela fila aproximadamente a cada 20 segundos e faz os POSTs em paralelo, até 100 entregas por passagem. A entrega chega, em geral, alguns segundos depois do evento.
- O seu sistema confere a assinatura, responde com HTTP 2xx e processa o evento.
- Se a resposta não for 2xx, ou a conexão falhar, o ChatISP tenta de novo mais tarde, com espera crescente. Veja Reenvio e prazos.
Requisitos
- Plano com API pública e webhooks (PREMIUM, GOLD, ENTERPRISE ou teste) Recurso de plano. Em outros planos, o card Webhooks aparece trancado.
- Permissão Integrações no perfil de acesso de quem cadastra.
- Um endereço HTTP ou HTTPS que o servidor do ChatISP consiga alcançar. Em instalação própria, o servidor precisa de acesso de saída a esse endereço; no ChatISP Cloud, o endereço precisa ser público na internet.
Não há limite de quantidade de webhooks. Um mesmo evento é entregue a todos os webhooks ativos inscritos nele, cada um com a sua fila e as suas tentativas.
Cadastrar um webhook
- No menu lateral, abra Integrações.
- No card Webhooks ("Avisos por HTTP para sistemas externos quando algo acontece no atendimento."), clique em Novo webhook.
- Preencha o Nome (até 120 caracteres), por exemplo "CRM: atendimentos".
- Em Endereço, informe a URL completa que vai receber os POSTs, começando com
https://(ouhttp://), com até 500 caracteres. - Marque os eventos. Num webhook novo vêm marcados Mensagem recebida, Conversa criada e Conversa encerrada; os de volume alto (Mensagem enviada e Entrega e leitura) ficam desmarcados até você precisar deles. Pelo menos um é obrigatório.
- Clique em Salvar. A janela Webhook criado mostra o Segredo, com o botão Copiar. Guarde-o no seu sistema: é com ele que você confere a assinatura de cada entrega.
whsec_4b1e9a07c3d2f68e5a90b1c2d3e4f5a6
O segredo começa com whsec_, seguido de 32 caracteres hexadecimais, e é gerado pelo ChatISP.
A lista de webhooks
| Coluna | O que mostra |
|---|---|
| Nome | O nome e, logo abaixo, o endereço (abreviado; o completo aparece ao passar o mouse). |
| Eventos | Uma etiqueta para cada evento inscrito. |
| Último disparo | Data e hora da última tentativa de entrega. Ao passar o mouse, aparece o resultado: http 200 no sucesso; o código HTTP ou o erro de conexão na falha. Quando há entregas aguardando, aparece um número em destaque ("3 entrega(s) na fila"). |
| Situação | Ativo ou Inativo. |
| Ações | Editar, Testar, Segredo (mostra o segredo de novo) e Excluir. |
Editar, pausar e excluir
- Editar muda nome, endereço e eventos, e mostra o switch Ativo. O segredo não muda.
- Desligar Ativo pausa o webhook: eventos novos deixam de ser enfileirados para ele, e as entregas que já estavam na fila ficam guardadas, sem gastar tentativas. Ao religar, elas voltam a sair.
- Excluir pede confirmação ("Excluir este webhook? As entregas pendentes serão descartadas.") e apaga o webhook com a fila dele.
O segredo não pode ser trocado. Para trocá-lo (por exemplo, se vazou), crie um webhook novo com o mesmo endereço e os mesmos eventos, atualize o segredo no seu sistema e exclua o antigo. Durante a troca, o seu sistema pode aceitar os dois segredos.
Eventos
São 9 eventos, na ordem da tela. O código é o valor que chega no campo evento e no cabeçalho X-ChatISP-Evento.
| Evento | Rótulo na tela | Quando dispara | Volume |
|---|---|---|---|
mensagem_recebida | Mensagem recebida | A cada mensagem que chega do cliente, em qualquer conexão e em qualquer estado da conversa (fila, fluxo, IA ou atendente), de qualquer tipo. | alto |
mensagem_enviada | Mensagem enviada | A cada mensagem que o provedor envia ao cliente: atendente no painel ou direto no celular pareado por QR Code, IA, fluxo, mensagens automáticas, campanhas, régua de cobrança e API. Também quando o canal recusa o envio (com status erro). | alto |
mensagem_status | Entrega e leitura | Quando uma mensagem enviada muda para entregue, lida ou erro, conforme o recibo do WhatsApp. | muito alto |
conversa_criada | Conversa criada | Quando nasce uma conversa nova, com protocolo novo, por qualquer caminho: o cliente escreveu sem conversa aberta, a equipe iniciou uma conversa pelo painel, envio ou abertura pela API, campanha ou régua de cobrança. | médio |
conversa_atribuida | Conversa atribuída | Quando um atendente passa a ser o dono da conversa sem transferência: assumiu da fila ou da IA, iniciou a conversa pelo painel, puxou a conversa de um colega, atendeu uma chamada de voz, ou a distribuição automática entregou a conversa a ele. | médio |
conversa_transferida | Conversa transferida | Quando a conversa é transferida para um departamento ou atendente: pelo painel, pela IA, por uma etapa de fluxo, pela API (conversas/transferir e atendimentos/abrir), pelo copiloto, ou quando um atendente responde numa conversa que estava com a IA. | médio |
conversa_encerrada | Conversa encerrada | Quando um atendimento é encerrado: pelo atendente, pela IA, por fluxo, por inatividade, pelo copiloto ou pela API. | médio |
ticket_criado | OS aberta | Quando uma OS é aberto: no painel (avulso ou a partir de uma conversa), pela IA, pelo copiloto ou pela API. | baixo |
ticket_atualizado | OS atualizado | Quando uma OS muda de etapa, é reaberto, muda de responsável ou recebe um comentário, por qualquer caminho. | baixo |
Além desses, o botão Testar entrega o evento especial teste (veja Testar).
O que não gera evento
- Notas internas da equipe e as notas gravadas pela API (
conversas/nota). - A transferência para um atendente gera só
conversa_transferida, nãoconversa_atribuida. Já a transferência para um departamento com distribuição automática ligada pode gerar as duas: primeiroconversa_transferida(para a fila) e depoisconversa_atribuidacom"como": "roleta", quando a distribuição entrega a conversa a alguém. - Nos ordens de serviço, edição de título, descrição, prioridade, prazo ou tipo, e anexos, não geram
ticket_atualizado. Abrir uma OS já com responsável gera sóticket_criado. - O encerramento automático das conversas abertas por campanhas e pela régua de cobrança (a conversa nova fica encerrada até o cliente responder) não gera
conversa_encerrada. O mesmo encerramento feito pela API gera, com"motivo": "aviso".
Número gêmeo
Num número ligado por QR e pela API oficial ao mesmo tempo, a mesma mensagem do cliente pode chegar pelos dois canais. O ChatISP registra uma só e emite um só mensagem_recebida. Os eventos trazem o conexao_id da conversa, em geral o da perna principal (veja Conexões).
Formato da entrega
Cada entrega é um POST para o endereço cadastrado, com corpo JSON em UTF-8:
POST /chatisp/webhook HTTP/1.1
Host: crm.seuprovedor.com.br
Content-Type: application/json; charset=utf-8
X-ChatISP-Evento: conversa_encerrada
X-ChatISP-Entrega: 90412
X-ChatISP-Assinatura: sha256=5d2c0f1e8b7a69c4e3d21f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c
{"evento":"conversa_encerrada","emitido_em":"2026-10-01T10:05:12-03:00","dados":{...}}Cabeçalhos
| Cabeçalho | Conteúdo |
|---|---|
Content-Type | application/json; charset=utf-8 |
X-ChatISP-Evento | O código do evento, igual ao campo evento do corpo. Permite rotear antes de decodificar o JSON. |
X-ChatISP-Entrega | Número único da entrega, o mesmo em todas as tentativas. Use-o para descartar duplicatas (veja Idempotência). No evento teste, vale 0. |
X-ChatISP-Assinatura | sha256= seguido do HMAC SHA-256 do corpo, em hexadecimal minúsculo, calculado com o segredo do webhook. Veja Assinatura. |
Envelope do corpo
| Campo | Tipo | Descrição |
|---|---|---|
evento | string | Código do evento. |
emitido_em | string | Quando o evento aconteceu, em ISO 8601 com fuso (2026-10-01T10:05:12-03:00). Não muda nas novas tentativas: o corpo reenviado é idêntico ao original, byte a byte. |
dados | objeto | Os dados do evento, descritos nas seções a seguir. |
Resposta esperada do seu sistema
- Qualquer HTTP 2xx (200, 201, 202, 204) confirma a entrega. O corpo da resposta é ignorado.
- Qualquer outro código (inclusive redirecionamentos 3xx, que não são seguidos), erro de conexão, erro de DNS ou demora acima de 10 segundos conta como falha, e a entrega volta para a fila.
- O ChatISP não recusa certificado autoassinado no seu endereço. Mesmo assim, use certificado válido: o corpo leva telefone, nome e conteúdo das mensagens dos seus clientes.
Responda rápido. As entregas de cada passagem saem em paralelo, então um endereço lento não atrasa os outros, mas o seu próprio sistema perde a entrega se passar de 10 segundos e ela volta para nova tentativa. Grave o evento numa fila do seu lado, responda 200 na hora e processe depois.
Dados dos eventos de mensagem
Resumo da conversa (comum a todos os eventos de mensagem e de conversa)
| Campo | Tipo | Descrição |
|---|---|---|
conversa_id | inteiro | Identificador da conversa, para usar em conversas/consultar, conversas/mensagens e nas rotas de gestão. |
protocolo | string | Protocolo da conversa (AAAAMMDD-NNNN). |
conexao_id | inteiro | Conexão (número de WhatsApp) da conversa. |
telefone | string | Telefone do cliente, só dígitos, com código do país. |
nome | string | Nome do cliente na conversa; vazio quando desconhecido. |
estado | string | Estado da conversa no momento do evento: fila, auto, ia, humano ou encerrada. |
mensagem_recebida
O resumo da conversa mais o objeto mensagem:
| Campo | Tipo | Descrição |
|---|---|---|
mensagem.id | inteiro | Identificador da mensagem. É o mesmo id de conversas/mensagens e de mensagens/arquivo. |
mensagem.tipo | string | texto, imagem, audio, video, documento, localizacao ou contato. |
mensagem.conteudo | string | O texto; em mídias, a legenda (pode vir vazio). O arquivo não vem no webhook: baixe com mensagens/arquivo?id=. Áudios chegam sem a transcrição, que é feita depois e aparece em conversas/mensagens. |
mensagem.recebida_em | string | Data e hora da mensagem informada pelo WhatsApp, em ISO 8601. |
{
"evento": "mensagem_recebida",
"emitido_em": "2026-10-01T09:12:41-03:00",
"dados": {
"conversa_id": 18342,
"protocolo": "20261001-0042",
"conexao_id": 1,
"telefone": "5544999990000",
"nome": "Maria Souza",
"estado": "fila",
"mensagem": {
"id": 402850,
"tipo": "texto",
"conteudo": "Minha internet caiu desde cedo",
"recebida_em": "2026-10-01T09:12:40-03:00"
}
}
}mensagem_enviada
O resumo da conversa mais o objeto mensagem:
| Campo | Tipo | Descrição |
|---|---|---|
mensagem.id | inteiro | Identificador da mensagem. Os eventos mensagem_status seguintes trazem o mesmo id. |
mensagem.autor | string | atendente (enviada pelo painel ou digitada direto no celular pareado por QR Code), ia (assistente de IA) ou sistema (fluxos, mensagens automáticas como encerramento, pesquisa e inatividade, campanhas, régua de cobrança e envios pela API). |
mensagem.autor_nome | string | Nome do atendente quando a mensagem foi enviada pelo painel; vazio nos demais casos, inclusive quando autor é atendente mas a mensagem foi digitada direto no celular pareado por QR Code. |
mensagem.tipo | string | Os mesmos tipos de mensagem_recebida. |
mensagem.conteudo | string | O texto como ficou registrado na conversa (sem a assinatura do atendente); em mídias, a legenda; em modelos, o corpo com as variáveis preenchidas. |
mensagem.nome_arquivo | string | Nome do arquivo nas mídias; vazio em texto. |
mensagem.status | string | enviada quando o canal aceitou; erro quando recusou na hora; pendente nos raros casos em que o canal aceitou sem devolver identificador. |
mensagem.erro | string | Motivo da recusa devolvido pelo canal quando status é erro; vazio nos demais. |
mensagem.enviada_em | string | Data e hora do registro do envio, em ISO 8601. |
{
"evento": "mensagem_enviada",
"emitido_em": "2026-10-01T09:14:02-03:00",
"dados": {
"conversa_id": 18342,
"protocolo": "20261001-0042",
"conexao_id": 1,
"telefone": "5544999990000",
"nome": "Maria Souza",
"estado": "humano",
"mensagem": {
"id": 402853,
"autor": "atendente",
"autor_nome": "Ana Ribeiro",
"tipo": "texto",
"conteudo": "Bom dia, Maria! Já estou verificando o seu sinal.",
"nome_arquivo": "",
"status": "enviada",
"erro": "",
"enviada_em": "2026-10-01T09:14:02-03:00"
}
}
}mensagem_status
O resumo da conversa mais um objeto mensagem curto. Só sai quando o status avança (enviada para entregue, entregue para lida) ou quando a mensagem passa a erro; recibos repetidos ou atrasados que não mudam nada não geram evento.
| Campo | Tipo | Descrição |
|---|---|---|
mensagem.id | inteiro | A mensagem enviada a que o recibo se refere (o mesmo id de mensagem_enviada e da resposta da API de envio). |
mensagem.status | string | entregue, lida ou erro. |
mensagem.em | string | Quando o ChatISP recebeu o recibo, em ISO 8601. |
{
"evento": "mensagem_status",
"emitido_em": "2026-10-01T09:15:30-03:00",
"dados": {
"conversa_id": 18342,
"protocolo": "20261001-0042",
"conexao_id": 1,
"telefone": "5544999990000",
"nome": "Maria Souza",
"estado": "humano",
"mensagem": {
"id": 402853,
"status": "lida",
"em": "2026-10-01T09:15:30-03:00"
}
}
}Quando o cliente lê uma mensagem, o WhatsApp considera lidas também as anteriores daquela conversa. O ChatISP marca todas como lidas na conversa, mas o evento mensagem_status sai só para a mensagem do recibo. Se você acompanha leitura por mensagem, trate um lida como leitura de tudo o que foi enviado antes na mesma conversa. Algumas pessoas desligam a confirmação de leitura no WhatsApp: nesse caso o status para em entregue.
Dados dos eventos de conversa
Todos começam pelo resumo da conversa descrito em Dados dos eventos de mensagem.
conversa_criada
Só o resumo da conversa. O estado é sempre fila: o evento sai no instante da criação, antes de fluxo, IA ou distribuição agirem. Quando quem abre é o cliente, chega em seguida o mensagem_recebida da primeira mensagem.
{
"evento": "conversa_criada",
"emitido_em": "2026-10-01T09:12:41-03:00",
"dados": {
"conversa_id": 18342,
"protocolo": "20261001-0042",
"conexao_id": 1,
"telefone": "5544999990000",
"nome": "Maria Souza",
"estado": "fila"
}
}conversa_atribuida e conversa_transferida
Os dois trazem o resumo da conversa e quem está com ela depois da mudança:
| Campo | Tipo | Descrição |
|---|---|---|
departamento_id | inteiro ou null | Departamento da conversa. |
departamento | string | Nome do departamento; vazio sem departamento. |
atendente_id | inteiro ou null | Atendente responsável; null quando a conversa foi para a fila. |
atendente | string | Nome do atendente; vazio quando na fila. |
como | string | Só em conversa_atribuida: roleta quando a distribuição automática entregou a conversa; assumida nos demais casos (o atendente assumiu, iniciou ou puxou a conversa). |
detalhe | string | Só em conversa_transferida: descrição da transferência para leitura humana, por exemplo "Fila do departamento: Financeiro", "Atendente: Ana Ribeiro (Suporte técnico)", "fluxo: Menu principal" ou "API: ERP financeiro (cliente pediu segunda via)". O texto pode mudar; não use para decisões automáticas. |
{
"evento": "conversa_transferida",
"emitido_em": "2026-10-01T09:20:15-03:00",
"dados": {
"conversa_id": 18342,
"protocolo": "20261001-0042",
"conexao_id": 1,
"telefone": "5544999990000",
"nome": "Maria Souza",
"estado": "fila",
"departamento_id": 2,
"departamento": "Financeiro",
"atendente_id": null,
"atendente": "",
"detalhe": "Fila do departamento: Financeiro"
}
}{
"evento": "conversa_atribuida",
"emitido_em": "2026-10-01T09:20:16-03:00",
"dados": {
"conversa_id": 18342,
"protocolo": "20261001-0042",
"conexao_id": 1,
"telefone": "5544999990000",
"nome": "Maria Souza",
"estado": "humano",
"departamento_id": 2,
"departamento": "Financeiro",
"atendente_id": 9,
"atendente": "Bruno Lima",
"como": "roleta"
}
}conversa_encerrada
O resumo da conversa (com estado encerrada) mais o campo motivo:
Valor de motivo | Quando |
|---|---|
| Nome de um motivo de encerramento | Encerrada com motivo escolhido (painel, IA ou API com motivo_id). Os nomes vêm de motivos/listar. |
| vazio | Encerrada no painel ou pela IA sem motivo. |
inatividade | Encerrada automaticamente por falta de resposta do cliente. |
fluxo: <nome do fluxo> | Encerrada por uma etapa de fluxo. |
API: <nome da chave> | Encerrada por conversas/encerrar sem motivo_id. |
aviso | Envio pela API para um número sem conversa aberta: a conversa nasce e fica encerrada até o cliente responder. |
| outros textos | Descrições de encerramentos automáticos (por exemplo, pelo copiloto ou quando a IA encerra a continuação de um atendimento recém-fechado). |
{
"evento": "conversa_encerrada",
"emitido_em": "2026-10-01T10:05:12-03:00",
"dados": {
"conversa_id": 18342,
"protocolo": "20261001-0042",
"conexao_id": 1,
"telefone": "5544999990000",
"nome": "Maria Souza",
"estado": "encerrada",
"motivo": "Problema resolvido"
}
}Para obter o histórico e o resumo do atendimento, chame conversas/consultar ou conversas/mensagens com o conversa_id. O resumo feito pela IA é gerado alguns instantes depois do encerramento; se o seu sistema precisa dele, consulte com um pequeno atraso.
Dados dos eventos de OS
ticket_criado e ticket_atualizado trazem o mesmo resumo da OS, já com os valores depois da mudança:
| Campo | Tipo | Descrição |
|---|---|---|
ticket_id | inteiro | Identificador da OS, para tickets/consultar e as rotas de gestão. |
numero | string | Número da OS como aparece no painel, por exemplo OS-000123. |
titulo | string | Título. |
etapa_id | inteiro | Etapa atual (ids de tickets/etapas). |
etapa | string | Nome da etapa atual. |
natureza | string | Natureza da etapa: inicial, andamento, aguardando, resolvido ou cancelado. As duas últimas encerram a OS. Use a natureza, e não o nome da etapa, nas regras do seu sistema. |
prioridade | string | baixa, normal, alta ou urgente. |
origem | string | Como foi aberto: manual (painel), chat (a partir de uma conversa), ia, copiloto ou api. |
conversa_id | inteiro ou null | Conversa de origem, quando houver. |
telefone | string | Telefone do cliente; vazio quando não informado. |
nome_cliente | string | Nome do cliente; vazio quando não informado. |
departamento_id | inteiro ou null | Departamento da OS. |
responsavel_id | inteiro ou null | Responsável; null sem dono. |
responsavel | string | Nome do responsável; vazio sem dono. |
prazo_em | string ou null | Prazo em ISO 8601, quando definido. |
ticket_atualizado acrescenta:
| Campo | Descrição |
|---|---|
mudanca | O que mudou: etapa (moveu de etapa), reaberto (saiu de uma etapa final para uma de trabalho), responsavel (atribuído, trocado ou retirado) ou comentario. |
comentario | Só quando mudanca é comentario: o texto, com até 2000 caracteres. Comentários feitos pela API começam com [API: nome da chave]. |
{
"evento": "ticket_criado",
"emitido_em": "2026-10-01T09:25:40-03:00",
"dados": {
"ticket_id": 123,
"numero": "OS-000123",
"titulo": "Sem sinal na ONU: visita técnica",
"etapa_id": 1,
"etapa": "Novo",
"natureza": "inicial",
"prioridade": "alta",
"origem": "ia",
"conversa_id": 18342,
"telefone": "5544999990000",
"nome_cliente": "Maria Souza",
"departamento_id": 1,
"responsavel_id": null,
"responsavel": "",
"prazo_em": "2026-10-02T18:00:00-03:00"
}
}{
"evento": "ticket_atualizado",
"emitido_em": "2026-10-01T14:02:10-03:00",
"dados": {
"ticket_id": 123,
"numero": "OS-000123",
"titulo": "Sem sinal na ONU: visita técnica",
"etapa_id": 3,
"etapa": "Em campo",
"natureza": "andamento",
"prioridade": "alta",
"origem": "ia",
"conversa_id": 18342,
"telefone": "5544999990000",
"nome_cliente": "Maria Souza",
"departamento_id": 1,
"responsavel_id": 12,
"responsavel": "Diego Técnico",
"prazo_em": "2026-10-02T18:00:00-03:00",
"mudanca": "comentario",
"comentario": "[API: Sistema de OS] OS 55821 aberta, técnico a caminho."
}
}Quando um usuário do painel move uma OS sem dono para uma etapa de trabalho, ele passa a ser o responsável automaticamente. Nesse caso sai um único ticket_atualizado com mudanca etapa, já com o responsavel preenchido.
Assinatura HMAC
Qualquer pessoa que descubra o endereço do seu webhook pode tentar enviar POSTs falsos. A assinatura garante que a entrega veio do seu ChatISP e que o corpo não foi alterado:
X-ChatISP-Assinatura = "sha256=" + hex( HMAC_SHA256( chave = segredo, mensagem = corpo_cru ) )
- Segredo: o valor inteiro mostrado na tela, incluindo o prefixo
whsec_. - Corpo cru: os bytes exatos recebidos, antes de qualquer decodificação. Não decodifique e codifique o JSON de novo para calcular: a nova serialização muda espaços, ordem ou escapes (o ChatISP envia acentos e barras sem escape) e a assinatura deixa de bater.
- Compare em tempo constante (
hash_equalsno PHP,crypto.timingSafeEqualno Node.js,hmac.compare_digestno Python). - Assinatura ausente ou diferente: responda
401e descarte.
<?php $segredo = getenv('CHATISP_WEBHOOK_SEGREDO'); // whsec_... $corpo = file_get_contents('php://input'); // corpo CRU $recebida = $_SERVER['HTTP_X_CHATISP_ASSINATURA'] ?? ''; $esperada = 'sha256=' . hash_hmac('sha256', $corpo, $segredo); if (!hash_equals($esperada, $recebida)) { http_response_code(401); exit; } $entrega = (int) ($_SERVER['HTTP_X_CHATISP_ENTREGA'] ?? 0); $evento = json_decode($corpo, true); if ($evento['evento'] !== 'teste') { $pdo = new PDO(getenv('DB_DSN'), getenv('DB_USER'), getenv('DB_PASS')); // chave única em "entrega": a repetição de uma entrega já gravada é ignorada $pdo->prepare('INSERT IGNORE INTO chatisp_eventos (entrega, evento, corpo) VALUES (?, ?, ?)') ->execute([$entrega, $evento['evento'], $corpo]); } http_response_code(200); // responde já; um processo separado trata a tabela
const express = require('express'); const crypto = require('crypto'); const SEGREDO = process.env.CHATISP_WEBHOOK_SEGREDO; // whsec_... const app = express(); function assinaturaValida(corpo, cabecalho) { const esperada = 'sha256=' + crypto.createHmac('sha256', SEGREDO).update(corpo).digest('hex'); const a = Buffer.from(esperada); const b = Buffer.from(cabecalho || ''); return a.length === b.length && crypto.timingSafeEqual(a, b); } // express.raw mantém o corpo como Buffer, sem decodificar app.post('/chatisp/webhook', express.raw({ type: 'application/json' }), (req, res) => { if (!assinaturaValida(req.body, req.get('X-ChatISP-Assinatura'))) { return res.sendStatus(401); } const entrega = req.get('X-ChatISP-Entrega'); const evento = JSON.parse(req.body.toString('utf8')); res.sendStatus(200); // responde antes de processar setImmediate(() => tratar(entrega, evento)); }); app.listen(3000);
import hmac, hashlib, json, os from flask import Flask, request SEGREDO = os.environ["CHATISP_WEBHOOK_SEGREDO"].encode() # whsec_... app = Flask(__name__) def assinatura_valida(corpo: bytes, cabecalho: str) -> bool: esperada = "sha256=" + hmac.new(SEGREDO, corpo, hashlib.sha256).hexdigest() return hmac.compare_digest(esperada, cabecalho or "") @app.post("/chatisp/webhook") def receber(): corpo = request.get_data() # bytes crus, sem decodificar if not assinatura_valida(corpo, request.headers.get("X-ChatISP-Assinatura")): return "", 401 entrega = request.headers.get("X-ChatISP-Entrega") evento = json.loads(corpo) enfileirar(entrega, evento) # sua fila (Redis, RQ, Celery, tabela...) return "", 200
Mais receptores completos estão em Exemplos práticos.
Outras proteções recomendadas
- Use HTTPS no endereço do webhook.
- Use um caminho difícil de adivinhar na URL (por exemplo
/hooks/chatisp-8f2k1q) como camada extra, nunca como substituto da assinatura. - Para descartar entregas antigas reenviadas por terceiros, compare
emitido_emcom o relógio do seu servidor, lembrando que as novas tentativas legítimas podem chegar até cerca de 7 horas e meia depois do evento. - Se o seu firewall libera só origens conhecidas, libere o endereço IP de saída do servidor do ChatISP (em instalação própria, o seu servidor; no ChatISP Cloud, consulte o suporte da Ravi Systems).
Reenvio e prazos
Quando uma entrega falha, o ChatISP agenda uma nova tentativa com espera crescente. São até 6 tentativas no total:
| Tentativa | Espera depois da falha anterior | Tempo aproximado desde o evento |
|---|---|---|
| 1ª | (primeira passagem da fila) | segundos |
| 2ª | 1 minuto | 1 minuto |
| 3ª | 5 minutos | 6 minutos |
| 4ª | 15 minutos | 21 minutos |
| 5ª | 1 hora | 1 hora e 21 minutos |
| 6ª e última | 6 horas | 7 horas e 21 minutos |
Os tempos podem se estender um pouco, porque a fila é verificada aproximadamente a cada 20 segundos e cada passagem entrega até 100 eventos. Se a 6ª tentativa também falhar, a entrega é abandonada e não volta mais.
- Queda longa = eventos perdidos. Se o seu sistema ficar fora do ar por mais de cerca de 7 horas, as entregas daquele período se esgotam. Depois de uma queda longa, reconcilie pela API:
conversas/listarcomatualizadas_desdeetickets/listartrazem o que mudou. - Manutenção planejada: pause o webhook (switch Ativo desligado). As entregas pendentes ficam congeladas sem gastar tentativas e recomeçam ao reativar. Eventos que acontecerem durante a pausa não são enfileirados; reconcilie esse intervalo pela API.
- Acompanhe a fila: o número em destaque na coluna Último disparo mostra quantas entregas aguardam. Número crescendo é sinal de que o seu sistema está recusando ou fora do ar.
Idempotência e ordem
Duplicatas
Uma entrega pode chegar mais de uma vez. Acontece quando o seu sistema processou o evento, mas a resposta não chegou a tempo (por exemplo, respondeu depois de 10 segundos, ou a conexão caiu antes da resposta): o ChatISP considera falha e tenta de novo.
Use o cabeçalho X-ChatISP-Entrega como chave de idempotência:
- Ele é o mesmo em todas as tentativas da mesma entrega, e o corpo também é idêntico.
- Ele é único por webhook: o mesmo evento entregue a dois webhooks tem números de entrega diferentes.
- Guarde os números já processados (uma tabela com chave única basta, como no exemplo em PHP acima) e ignore os repetidos, respondendo 200.
Como segunda linha de defesa, use os identificadores do próprio evento: mensagem.id para mensagens, conversa_id com o tipo de evento para conversas, ticket_id para ordens de serviço.
Ordem de chegada
Em condições normais, as entregas saem aproximadamente na ordem dos eventos (por exemplo, conversa_criada antes do mensagem_recebida da primeira mensagem). Mas não há garantia: eventos do mesmo segundo podem sair em outra ordem, e uma entrega que falha e vai para nova tentativa é ultrapassada pelas seguintes. Regras práticas:
- Ordene por
emitido_eme pelos ids, nunca pela ordem de chegada. - Para o estado atual de uma conversa ou OS, não acumule os eventos: use o evento como gatilho e consulte o estado na API (
conversas/consultar,tickets/consultar). - Num
mensagem_status, só avance o status do seu lado (enviada,entregue,lida); nunca volte delidaparaentreguepor causa de um evento atrasado.
Testar e acompanhar
Botão Testar
Na lista de webhooks, Testar faz na hora uma entrega do evento teste, fora da fila e sem novas tentativas, com a mesma assinatura e os mesmos cabeçalhos das entregas reais (o X-ChatISP-Entrega vale 0):
{"evento":"teste","emitido_em":"2026-10-01T08:30:00-03:00","dados":{"webhook_id":2}}O resultado aparece na tela:
- "Entrega de teste aceita (http 200)." quando o seu sistema respondeu 2xx.
- "Entrega de teste falhou (...)." com o código HTTP ou o erro de conexão, por exemplo
http 401(a assinatura não bateu no seu lado) ou uma mensagem de tempo esgotado ou de nome não encontrado.
O teste funciona mesmo com o webhook inativo e atualiza a coluna Último disparo. O seu sistema deve responder 2xx ao evento teste e não fazer nada com ele.
Testar com eventos reais
- Cadastre o webhook com os eventos desejados.
- Mande uma mensagem de um celular seu para o número do provedor: chegam
conversa_criada(se não havia conversa aberta) emensagem_recebida. - Responda pelo painel: chegam
conversa_atribuida(ao assumir),mensagem_enviadae, em seguida,mensagem_statuscomentregueelida. - Transfira e encerre a conversa para ver
conversa_transferidaeconversa_encerrada. Abra e mova uma OS para ver os eventos de OS.
Sem receptor pronto
Para ver o formato real antes de programar, cadastre temporariamente o endereço de um serviço de inspeção de requisições (do tipo "request bin") e clique em Testar. Depois troque pelo endereço definitivo. Não deixe um serviço de terceiros cadastrado em produção: as entregas reais levam dados dos seus clientes.
Eventos de volume alto
mensagem_enviada e mensagem_status acompanham cada mensagem enviada pelo provedor, inclusive as da IA, dos fluxos, das campanhas e da régua de cobrança. Cada mensagem enviada gera um mensagem_enviada e normalmente mais um ou dois mensagem_status (entregue e lida). Por isso eles vêm desmarcados num webhook novo.
Capacidade de entrega
O ChatISP entrega até 100 eventos por passagem da fila, em paralelo, e a fila passa aproximadamente a cada 20 segundos: algo da ordem de 300 entregas por minuto, somando todos os webhooks. Picos acima disso não se perdem, mas formam fila e chegam com atraso. Exemplo: uma campanha de 1.000 mensagens com os dois eventos de volume alto ligados gera cerca de 3.000 entregas, que levam em torno de 10 minutos para escoar; nesse intervalo, os eventos de conversa também esperam na mesma fila.
Recomendações
- Ligue
mensagem_enviadaemensagem_statussó se o seu sistema realmente usa essas informações (por exemplo, para comprovar a entrega de um aviso de cobrança). - Separe em webhooks diferentes os eventos de volume alto e os de conversa e OS, apontando para endereços diferentes. Cada webhook tem as suas próprias tentativas, e o sistema que trata conversas e ordens de serviço não precisa receber nem descartar milhares de recibos.
- Para confirmar a entrega de um envio feito pela própria API, também dá para consultar o
statusda mensagem comconversas/mensagensem vez de assinar todos os recibos. - Responda em milissegundos: com volume alto, um receptor lento acumula falhas por tempo esgotado e as entregas dele voltam para nova tentativa, chegando atrasadas.
Boas práticas e problemas frequentes
Checklist do receptor
- Validar a assinatura com o corpo cru, em tempo constante.
- Responder 2xx rápido, bem abaixo de 10 segundos; processar numa fila própria, em segundo plano.
- Descartar duplicatas pelo
X-ChatISP-Entrega. - Tratar o evento
testecom 2xx e sem efeito. - Ignorar eventos e campos desconhecidos, para continuar funcionando se o ChatISP acrescentar informações.
- Não depender da ordem de chegada; consultar a API quando precisar do estado atual.
- Inscrever só os eventos necessários e separar os de volume alto.
- Usar HTTPS com certificado válido.
Problemas frequentes
| Sintoma | Causa provável e solução |
|---|---|
Teste falha com http 401 ou http 403 | O seu receptor recusou. Normalmente é assinatura calculada sobre o JSON reserializado, ou segredo copiado sem o prefixo whsec_. |
Teste falha com http 301 ou http 302 | O endereço redireciona (de http:// para https://, ou por causa de uma barra no final). Redirecionamentos não são seguidos: cadastre o endereço final. |
| Teste falha com tempo esgotado | O seu sistema demorou mais de 10 segundos, ou o servidor do ChatISP não alcança o endereço (firewall, rede interna, DNS). |
| Teste passa, mas eventos reais não chegam | Webhook inativo, evento não marcado, ou o caso testado não gera o evento (veja O que não gera evento). Lembre que as entregas chegam alguns segundos depois. |
| O mesmo evento chegou duas vezes | Comportamento esperado após uma falha ou resposta lenta. Descarte pelo X-ChatISP-Entrega. |
| Eventos chegando com minutos de atraso | Fila acumulada por eventos de volume alto ligados durante uma campanha, ou entregas em nova tentativa porque o receptor demorou mais de 10 segundos. Veja Eventos de volume alto. |
| Número de entregas na fila não para de crescer | O seu sistema está recusando ou fora do ar. Veja o resultado em Último disparo, corrija, e as próximas tentativas entregam o acumulado. |