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

  1. Você cadastra um webhook com o endereço do seu sistema e os eventos de interesse.
  2. 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.
  3. 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.
  4. O seu sistema confere a assinatura, responde com HTTP 2xx e processa o evento.
  5. 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

  1. No menu lateral, abra Integrações.
  2. No card Webhooks ("Avisos por HTTP para sistemas externos quando algo acontece no atendimento."), clique em Novo webhook.
  3. Preencha o Nome (até 120 caracteres), por exemplo "CRM: atendimentos".
  4. Em Endereço, informe a URL completa que vai receber os POSTs, começando com https:// (ou http://), com até 500 caracteres.
  5. 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.
  6. 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.
formato do segredo
whsec_4b1e9a07c3d2f68e5a90b1c2d3e4f5a6

O segredo começa com whsec_, seguido de 32 caracteres hexadecimais, e é gerado pelo ChatISP.

A lista de webhooks

ColunaO que mostra
NomeO nome e, logo abaixo, o endereço (abreviado; o completo aparece ao passar o mouse).
EventosUma etiqueta para cada evento inscrito.
Último disparoData 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çãoAtivo ou Inativo.
AçõesEditar, 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.

EventoRótulo na telaQuando disparaVolume
mensagem_recebidaMensagem recebidaA 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_enviadaMensagem enviadaA 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_statusEntrega e leituraQuando uma mensagem enviada muda para entregue, lida ou erro, conforme o recibo do WhatsApp.muito alto
conversa_criadaConversa criadaQuando 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_atribuidaConversa atribuídaQuando 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_transferidaConversa transferidaQuando 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_encerradaConversa encerradaQuando um atendimento é encerrado: pelo atendente, pela IA, por fluxo, por inatividade, pelo copiloto ou pela API.médio
ticket_criadoOS abertaQuando uma OS é aberto: no painel (avulso ou a partir de uma conversa), pela IA, pelo copiloto ou pela API.baixo
ticket_atualizadoOS atualizadoQuando 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ão conversa_atribuida. Já a transferência para um departamento com distribuição automática ligada pode gerar as duas: primeiro conversa_transferida (para a fila) e depois conversa_atribuida com "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:

requisição HTTP
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çalhoConteúdo
Content-Typeapplication/json; charset=utf-8
X-ChatISP-EventoO código do evento, igual ao campo evento do corpo. Permite rotear antes de decodificar o JSON.
X-ChatISP-EntregaNú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-Assinaturasha256= seguido do HMAC SHA-256 do corpo, em hexadecimal minúsculo, calculado com o segredo do webhook. Veja Assinatura.

Envelope do corpo

CampoTipoDescrição
eventostringCódigo do evento.
emitido_emstringQuando 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.
dadosobjetoOs 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)

CampoTipoDescrição
conversa_idinteiroIdentificador da conversa, para usar em conversas/consultar, conversas/mensagens e nas rotas de gestão.
protocolostringProtocolo da conversa (AAAAMMDD-NNNN).
conexao_idinteiroConexão (número de WhatsApp) da conversa.
telefonestringTelefone do cliente, só dígitos, com código do país.
nomestringNome do cliente na conversa; vazio quando desconhecido.
estadostringEstado da conversa no momento do evento: fila, auto, ia, humano ou encerrada.

mensagem_recebida

O resumo da conversa mais o objeto mensagem:

CampoTipoDescrição
mensagem.idinteiroIdentificador da mensagem. É o mesmo id de conversas/mensagens e de mensagens/arquivo.
mensagem.tipostringtexto, imagem, audio, video, documento, localizacao ou contato.
mensagem.conteudostringO 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_emstringData e hora da mensagem informada pelo WhatsApp, em ISO 8601.
corpo
{
  "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:

CampoTipoDescrição
mensagem.idinteiroIdentificador da mensagem. Os eventos mensagem_status seguintes trazem o mesmo id.
mensagem.autorstringatendente (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_nomestringNome 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.tipostringOs mesmos tipos de mensagem_recebida.
mensagem.conteudostringO 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_arquivostringNome do arquivo nas mídias; vazio em texto.
mensagem.statusstringenviada quando o canal aceitou; erro quando recusou na hora; pendente nos raros casos em que o canal aceitou sem devolver identificador.
mensagem.errostringMotivo da recusa devolvido pelo canal quando status é erro; vazio nos demais.
mensagem.enviada_emstringData e hora do registro do envio, em ISO 8601.
corpo
{
  "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.

CampoTipoDescrição
mensagem.idinteiroA mensagem enviada a que o recibo se refere (o mesmo id de mensagem_enviada e da resposta da API de envio).
mensagem.statusstringentregue, lida ou erro.
mensagem.emstringQuando o ChatISP recebeu o recibo, em ISO 8601.
corpo
{
  "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.

corpo
{
  "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:

CampoTipoDescrição
departamento_idinteiro ou nullDepartamento da conversa.
departamentostringNome do departamento; vazio sem departamento.
atendente_idinteiro ou nullAtendente responsável; null quando a conversa foi para a fila.
atendentestringNome do atendente; vazio quando na fila.
comostringSó 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).
detalhestringSó 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.
corpo de conversa_transferida
{
  "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"
  }
}
corpo de conversa_atribuida
{
  "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 motivoQuando
Nome de um motivo de encerramentoEncerrada com motivo escolhido (painel, IA ou API com motivo_id). Os nomes vêm de motivos/listar.
vazioEncerrada no painel ou pela IA sem motivo.
inatividadeEncerrada 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.
avisoEnvio pela API para um número sem conversa aberta: a conversa nasce e fica encerrada até o cliente responder.
outros textosDescrições de encerramentos automáticos (por exemplo, pelo copiloto ou quando a IA encerra a continuação de um atendimento recém-fechado).
corpo
{
  "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:

CampoTipoDescrição
ticket_idinteiroIdentificador da OS, para tickets/consultar e as rotas de gestão.
numerostringNúmero da OS como aparece no painel, por exemplo OS-000123.
titulostringTítulo.
etapa_idinteiroEtapa atual (ids de tickets/etapas).
etapastringNome da etapa atual.
naturezastringNatureza 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.
prioridadestringbaixa, normal, alta ou urgente.
origemstringComo foi aberto: manual (painel), chat (a partir de uma conversa), ia, copiloto ou api.
conversa_idinteiro ou nullConversa de origem, quando houver.
telefonestringTelefone do cliente; vazio quando não informado.
nome_clientestringNome do cliente; vazio quando não informado.
departamento_idinteiro ou nullDepartamento da OS.
responsavel_idinteiro ou nullResponsável; null sem dono.
responsavelstringNome do responsável; vazio sem dono.
prazo_emstring ou nullPrazo em ISO 8601, quando definido.

ticket_atualizado acrescenta:

CampoDescrição
mudancaO que mudou: etapa (moveu de etapa), reaberto (saiu de uma etapa final para uma de trabalho), responsavel (atribuído, trocado ou retirado) ou comentario.
comentarioSó quando mudanca é comentario: o texto, com até 2000 caracteres. Comentários feitos pela API começam com [API: nome da chave].
corpo de ticket_criado
{
  "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"
  }
}
corpo de ticket_atualizado (comentário)
{
  "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:

regra
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_equals no PHP, crypto.timingSafeEqual no Node.js, hmac.compare_digest no Python).
  • Assinatura ausente ou diferente: responda 401 e descarte.
PHP (receptor completo)
<?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
Node.js (Express)
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);
Python (Flask)
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_em com 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:

TentativaEspera depois da falha anteriorTempo aproximado desde o evento
1ª(primeira passagem da fila)segundos
2ª1 minuto1 minuto
3ª5 minutos6 minutos
4ª15 minutos21 minutos
5ª1 hora1 hora e 21 minutos
6ª e última6 horas7 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/listar com atualizadas_desde e tickets/listar trazem 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_em e 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 de lida para entregue por 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):

corpo do evento teste
{"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

  1. Cadastre o webhook com os eventos desejados.
  2. Mande uma mensagem de um celular seu para o número do provedor: chegam conversa_criada (se não havia conversa aberta) e mensagem_recebida.
  3. Responda pelo painel: chegam conversa_atribuida (ao assumir), mensagem_enviada e, em seguida, mensagem_status com entregue e lida.
  4. Transfira e encerre a conversa para ver conversa_transferida e conversa_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_enviada e mensagem_status só 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 status da mensagem com conversas/mensagens em 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 teste com 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

SintomaCausa provável e solução
Teste falha com http 401 ou http 403O seu receptor recusou. Normalmente é assinatura calculada sobre o JSON reserializado, ou segredo copiado sem o prefixo whsec_.
Teste falha com http 301 ou http 302O 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 esgotadoO 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 chegamWebhook 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 vezesComportamento esperado após uma falha ou resposta lenta. Descarte pelo X-ChatISP-Entrega.
Eventos chegando com minutos de atrasoFila 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 crescerO seu sistema está recusando ou fora do ar. Veja o resultado em Último disparo, corrija, e as próximas tentativas entregam o acumulado.