Webhooks de salida
Reciba en su sistema un aviso HTTP cuando algo sucede en la atención: mensaje recibido, enviado, entregado o leído; conversación creada, asignada, transferida o cerrada; orden de servicio abierta o actualizada. Cada entrega es un POST con cuerpo JSON, firmado con HMAC SHA-256, y ChatISP reintenta por su cuenta cuando su sistema está fuera de servicio.
Visión general
Los webhooks son el camino inverso de la API: en lugar de que su sistema le pregunte a ChatISP qué cambió, ChatISP avisa a su sistema cuando sucede. Usos típicos en un proveedor de internet:
- Registrar en el CRM cada atención cerrada, con protocolo y motivo, y obtener el historial por la API.
- Saber cuándo el cliente respondió a un aviso enviado por el ERP y activar una automatización.
- Seguir si el boleto enviado por WhatsApp fue entregado y leído.
- Reflejar las órdenes de servicio en el ERP y saber cuándo cambian de etapa o de responsable.
- Alimentar un panel propio con la cola y la distribución de las atenciones en tiempo real.
Cómo funciona
- Usted registra un webhook con la dirección de su sistema y los eventos de interés.
- Cuando ocurre un evento, ChatISP guarda la entrega en una cola. La atención nunca espera a su sistema: si está lento o fuera de servicio, nada cambia para el equipo ni para el cliente.
- El servicio en segundo plano de ChatISP recorre la cola aproximadamente cada 20 segundos y hace los POST en paralelo, hasta 100 entregas por pasada. La entrega llega, en general, algunos segundos después del evento.
- Su sistema verifica la firma, responde con HTTP 2xx y procesa el evento.
- Si la respuesta no es 2xx, o la conexión falla, ChatISP intenta de nuevo más tarde, con espera creciente. Vea Reenvío y plazos.
Requisitos
- Plan con API pública y webhooks (PREMIUM, GOLD, ENTERPRISE o prueba) Recurso del plan. En otros planes, la tarjeta Webhooks aparece bloqueada.
- Permiso Integraciones en el perfil de acceso de quien registra.
- Una dirección HTTP o HTTPS que el servidor de ChatISP pueda alcanzar. En una instalación propia, el servidor necesita acceso de salida a esa dirección; en ChatISP Cloud, la dirección debe ser pública en internet.
No hay límite en la cantidad de webhooks. Un mismo evento se entrega a todos los webhooks activos suscritos a él, cada uno con su propia cola y sus propios intentos.
Registrar un webhook
- En el menú lateral, abra Integraciones.
- En la tarjeta Webhooks ("Avisos por HTTP a sistemas externos cuando algo ocurre en la atención."), haga clic en Nuevo webhook.
- Complete el Nombre (hasta 120 caracteres), por ejemplo "CRM: atenciones".
- En Dirección, informe la URL completa que recibirá los POST, comenzando con
https://(ohttp://), con hasta 500 caracteres. - Marque los eventos. En un webhook nuevo vienen marcados Mensaje recibido, Conversación creada y Conversación cerrada; los de volumen alto (Mensaje enviado y Entrega y lectura) quedan desmarcados hasta que los necesite. Al menos uno es obligatorio.
- Haga clic en Guardar. La ventana Webhook creado muestra el Secreto, con el botón Copiar. Guárdelo en su sistema: con él se verifica la firma de cada entrega.
whsec_4b1e9a07c3d2f68e5a90b1c2d3e4f5a6
El secreto comienza con whsec_, seguido de 32 caracteres hexadecimales, y lo genera ChatISP.
La lista de webhooks
| Columna | Qué muestra |
|---|---|
| Nombre | El nombre y, justo debajo, la dirección (abreviada; la completa aparece al pasar el mouse). |
| Eventos | Una etiqueta por cada evento suscrito. |
| Último envío | Fecha y hora del último intento de entrega. Al pasar el mouse aparece el resultado: http 200 en caso de éxito; el código HTTP o el error de conexión en caso de falla. Cuando hay entregas en espera, aparece un número destacado ("3 entrega(s) en cola"). |
| Situación | Activo o Inactivo. |
| Acciones | Editar, Probar, Secreto (muestra el secreto de nuevo) y Eliminar. |
Editar, pausar y eliminar
- Editar cambia nombre, dirección y eventos, y muestra el interruptor Activo. El secreto no cambia.
- Desactivar Activo pausa el webhook: los eventos nuevos dejan de encolarse para él, y las entregas que ya estaban en la cola quedan guardadas, sin gastar intentos. Al reactivarlo, vuelven a salir.
- Eliminar pide confirmación ("¿Eliminar este webhook? Las entregas pendientes se descartarán.") y borra el webhook junto con su cola.
El secreto no se puede cambiar. Para reemplazarlo (por ejemplo, si se filtró), cree un webhook nuevo con la misma dirección y los mismos eventos, actualice el secreto en su sistema y elimine el anterior. Durante el cambio, su sistema puede aceptar ambos secretos.
Eventos
Son 9 eventos, en el orden de la pantalla. El código es el valor que llega en el campo evento y en el encabezado X-ChatISP-Evento.
| Evento | Etiqueta en pantalla | Cuándo se dispara | Volumen |
|---|---|---|---|
mensagem_recebida | Mensaje recibido | Con cada mensaje que llega del cliente, en cualquier conexión y en cualquier estado de la conversación (cola, flujo, IA o agente), de cualquier tipo. | alto |
mensagem_enviada | Mensaje enviado | Con cada mensaje que el proveedor envía al cliente: agente en el panel o directamente en el celular vinculado por código QR, IA, flujo, mensajes automáticos, campañas, regla de cobranza y API. También cuando el canal rechaza el envío (con status erro). | alto |
mensagem_status | Entrega y lectura | Cuando un mensaje enviado cambia a entregue, lida o erro, según el recibo de WhatsApp. | muy alto |
conversa_criada | Conversación creada | Cuando nace una conversación nueva, con protocolo nuevo, por cualquier camino: el cliente escribió sin conversación abierta, el equipo inició una conversación desde el panel, envío o apertura por la API, campaña o regla de cobranza. | medio |
conversa_atribuida | Conversación asignada | Cuando un agente pasa a ser el dueño de la conversación sin transferencia: la tomó de la cola o de la IA, inició la conversación desde el panel, tomó la conversación de un colega, atendió una llamada de voz, o la distribución automática le entregó la conversación. | medio |
conversa_transferida | Conversación transferida | Cuando la conversación se transfiere a un departamento o agente: desde el panel, por la IA, por una etapa de flujo, por la API (conversas/transferir y atendimentos/abrir), por el copiloto, o cuando un agente responde en una conversación que estaba con la IA. | medio |
conversa_encerrada | Conversación cerrada | Cuando se cierra una atención: por el agente, por la IA, por flujo, por inactividad, por el copiloto o por la API. | medio |
ticket_criado | OS abierta | Cuando se abre una OS: en el panel (suelto o a partir de una conversación), por la IA, por el copiloto o por la API. | bajo |
ticket_atualizado | OS actualizada | Cuando una OS cambia de etapa, se reabre, cambia de responsable o recibe un comentario, por cualquier camino. | bajo |
Además de estos, el botón Probar entrega el evento especial teste (vea Probar).
Lo que no genera evento
- Las notas internas del equipo y las notas registradas por la API (
conversas/nota). - La transferencia a un agente genera solo
conversa_transferida, noconversa_atribuida. En cambio, la transferencia a un departamento con distribución automática activada puede generar ambos: primeroconversa_transferida(hacia la cola) y despuésconversa_atribuidacon"como": "roleta", cuando la distribución entrega la conversación a alguien. - En las órdenes de servicio, la edición de título, descripción, prioridad, plazo o tipo, y los adjuntos, no generan
ticket_atualizado. Abrir una OS que ya tiene responsable genera soloticket_criado. - El cierre automático de las conversaciones abiertas por campañas y por la regla de cobranza (la conversación nueva queda cerrada hasta que el cliente responda) no genera
conversa_encerrada. El mismo cierre hecho por la API sí lo genera, con"motivo": "aviso".
Número gemelo
En un número conectado por QR y por la API oficial al mismo tiempo, el mismo mensaje del cliente puede llegar por los dos canales. ChatISP registra uno solo y emite un solo mensagem_recebida. Los eventos traen el conexao_id de la conversación, en general el de la conexión principal (vea Conexiones).
Formato de la entrega
Cada entrega es un POST a la dirección registrada, con cuerpo JSON en UTF-8:
POST /chatisp/webhook HTTP/1.1
Host: crm.suproveedor.com
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":{...}}Encabezados
| Encabezado | Contenido |
|---|---|
Content-Type | application/json; charset=utf-8 |
X-ChatISP-Evento | El código del evento, igual al campo evento del cuerpo. Permite enrutar antes de decodificar el JSON. |
X-ChatISP-Entrega | Número único de la entrega, el mismo en todos los intentos. Úselo para descartar duplicados (vea Idempotencia). En el evento teste, vale 0. |
X-ChatISP-Assinatura | sha256= seguido del HMAC SHA-256 del cuerpo, en hexadecimal minúsculo, calculado con el secreto del webhook. Vea Firma. |
Sobre del cuerpo
| Campo | Tipo | Descripción |
|---|---|---|
evento | string | Código del evento. |
emitido_em | string | Cuándo ocurrió el evento, en ISO 8601 con zona horaria (2026-10-01T10:05:12-03:00). No cambia en los nuevos intentos: el cuerpo reenviado es idéntico al original, byte por byte. |
dados | objeto | Los datos del evento, descritos en las secciones siguientes. |
Respuesta esperada de su sistema
- Cualquier HTTP 2xx (200, 201, 202, 204) confirma la entrega. El cuerpo de la respuesta se ignora.
- Cualquier otro código (incluidas las redirecciones 3xx, que no se siguen), error de conexión, error de DNS o demora superior a 10 segundos cuenta como falla, y la entrega vuelve a la cola.
- ChatISP no rechaza un certificado autofirmado en su dirección. Aun así, use un certificado válido: el cuerpo lleva teléfono, nombre y contenido de los mensajes de sus clientes.
Responda rápido. Las entregas de cada pasada salen en paralelo, así que una dirección lenta no retrasa a las demás, pero su propio sistema pierde la entrega si supera los 10 segundos, y esta vuelve para un nuevo intento. Guarde el evento en una cola de su lado, responda 200 de inmediato y procese después.
Datos de los eventos de mensaje
Resumen de la conversación (común a todos los eventos de mensaje y de conversación)
| Campo | Tipo | Descripción |
|---|---|---|
conversa_id | entero | Identificador de la conversación, para usar en conversas/consultar, conversas/mensagens y en las rutas de gestión. |
protocolo | string | Protocolo de la conversación (AAAAMMDD-NNNN). |
conexao_id | entero | Conexión (número de WhatsApp) de la conversación. |
telefone | string | Teléfono del cliente, solo dígitos, con código de país. |
nome | string | Nombre del cliente en la conversación; vacío cuando es desconocido. |
estado | string | Estado de la conversación en el momento del evento: fila, auto, ia, humano o encerrada. |
mensagem_recebida
El resumen de la conversación más el objeto mensagem:
| Campo | Tipo | Descripción |
|---|---|---|
mensagem.id | entero | Identificador del mensaje. Es el mismo id de conversas/mensagens y de mensagens/arquivo. |
mensagem.tipo | string | texto, imagem, audio, video, documento, localizacao o contato. |
mensagem.conteudo | string | El texto; en medios, la leyenda (puede venir vacía). El archivo no viene en el webhook: descárguelo con mensagens/arquivo?id=. Los audios llegan sin la transcripción, que se hace después y aparece en conversas/mensagens. |
mensagem.recebida_em | string | Fecha y hora del mensaje informada por WhatsApp, en 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": "Mi internet está caída desde temprano",
"recebida_em": "2026-10-01T09:12:40-03:00"
}
}
}mensagem_enviada
El resumen de la conversación más el objeto mensagem:
| Campo | Tipo | Descripción |
|---|---|---|
mensagem.id | entero | Identificador del mensaje. Los eventos mensagem_status siguientes traen el mismo id. |
mensagem.autor | string | atendente (enviado desde el panel o escrito directamente en el celular vinculado por código QR), ia (asistente de IA) o sistema (flujos, mensajes automáticos como cierre, encuesta e inactividad, campañas, regla de cobranza y envíos por la API). |
mensagem.autor_nome | string | Nombre del agente cuando el mensaje se envió desde el panel; vacío en los demás casos, incluso cuando autor es atendente pero el mensaje se escribió directamente en el celular vinculado por código QR. |
mensagem.tipo | string | Los mismos tipos de mensagem_recebida. |
mensagem.conteudo | string | El texto tal como quedó registrado en la conversación (sin la firma del agente); en medios, la leyenda; en plantillas, el cuerpo con las variables completadas. |
mensagem.nome_arquivo | string | Nombre del archivo en los medios; vacío en texto. |
mensagem.status | string | enviada cuando el canal lo aceptó; erro cuando lo rechazó en el momento; pendente en los raros casos en que el canal lo aceptó sin devolver identificador. |
mensagem.erro | string | Motivo del rechazo devuelto por el canal cuando status es erro; vacío en los demás casos. |
mensagem.enviada_em | string | Fecha y hora del registro del envío, en 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": "¡Buenos días, Maria! Ya estoy revisando su señal.",
"nome_arquivo": "",
"status": "enviada",
"erro": "",
"enviada_em": "2026-10-01T09:14:02-03:00"
}
}
}mensagem_status
El resumen de la conversación más un objeto mensagem corto. Solo sale cuando el status avanza (enviada a entregue, entregue a lida) o cuando el mensaje pasa a erro; los recibos repetidos o atrasados que no cambian nada no generan evento.
| Campo | Tipo | Descripción |
|---|---|---|
mensagem.id | entero | El mensaje enviado al que se refiere el recibo (el mismo id de mensagem_enviada y de la respuesta de la API de envío). |
mensagem.status | string | entregue, lida o erro. |
mensagem.em | string | Cuándo ChatISP recibió el recibo, en 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"
}
}
}Cuando el cliente lee un mensaje, WhatsApp considera leídos también los anteriores de esa conversación. ChatISP los marca todos como leídos en la conversación, pero el evento mensagem_status sale solo para el mensaje del recibo. Si usted sigue la lectura por mensaje, trate un lida como lectura de todo lo enviado antes en la misma conversación. Algunas personas desactivan la confirmación de lectura en WhatsApp: en ese caso el status se detiene en entregue.
Datos de los eventos de conversación
Todos comienzan con el resumen de la conversación descrito en Datos de los eventos de mensaje.
conversa_criada
Solo el resumen de la conversación. El estado es siempre fila: el evento sale en el instante de la creación, antes de que actúen el flujo, la IA o la distribución. Cuando quien la abre es el cliente, llega a continuación el mensagem_recebida del primer mensaje.
{
"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 y conversa_transferida
Ambos traen el resumen de la conversación y quién la tiene después del cambio:
| Campo | Tipo | Descripción |
|---|---|---|
departamento_id | entero o null | Departamento de la conversación. |
departamento | string | Nombre del departamento; vacío sin departamento. |
atendente_id | entero o null | Agente responsable; null cuando la conversación fue a la cola. |
atendente | string | Nombre del agente; vacío cuando está en la cola. |
como | string | Solo en conversa_atribuida: roleta cuando la distribución automática entregó la conversación; assumida en los demás casos (el agente tomó, inició o se llevó la conversación). |
detalhe | string | Solo en conversa_transferida: descripción de la transferencia para lectura humana, por ejemplo "Cola del departamento: Financiero", "Agente: Ana Ribeiro (Soporte técnico)", "flujo: Menú principal" o "API: ERP financiero (el cliente pidió segunda vía)". El texto puede cambiar; no lo use para decisiones 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": "Financiero",
"atendente_id": null,
"atendente": "",
"detalhe": "Cola del departamento: Financiero"
}
}{
"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": "Financiero",
"atendente_id": 9,
"atendente": "Bruno Lima",
"como": "roleta"
}
}conversa_encerrada
El resumen de la conversación (con estado encerrada) más el campo motivo:
Valor de motivo | Cuándo |
|---|---|
| Nombre de un motivo de cierre | Cerrada con motivo elegido (panel, IA o API con motivo_id). Los nombres vienen de motivos/listar. |
| vacío | Cerrada en el panel o por la IA sin motivo. |
inatividade | Cerrada automáticamente por falta de respuesta del cliente. |
fluxo: <nombre del flujo> | Cerrada por una etapa de flujo. |
API: <nombre de la clave> | Cerrada por conversas/encerrar sin motivo_id. |
aviso | Envío por la API a un número sin conversación abierta: la conversación nace y queda cerrada hasta que el cliente responda. |
| otros textos | Descripciones de cierres automáticos (por ejemplo, por el copiloto o cuando la IA cierra la continuación de una atención recién cerrada). |
{
"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 resuelto"
}
}Para obtener el historial y el resumen de la atención, llame a conversas/consultar o conversas/mensagens con el conversa_id. El resumen hecho por la IA se genera unos instantes después del cierre; si su sistema lo necesita, consulte con un pequeño retraso.
Datos de los eventos de orden de servicio
ticket_criado y ticket_atualizado traen el mismo resumen de la OS, ya con los valores después del cambio:
| Campo | Tipo | Descripción |
|---|---|---|
ticket_id | entero | Identificador de la OS, para tickets/consultar y las rutas de gestión. |
numero | string | Número de la OS tal como aparece en el panel, por ejemplo OS-000123. |
titulo | string | Título. |
etapa_id | entero | Etapa actual (ids de tickets/etapas). |
etapa | string | Nombre de la etapa actual. |
natureza | string | Naturaleza de la etapa: inicial, andamento, aguardando, resolvido o cancelado. Las dos últimas cierran la OS. Use la naturaleza, y no el nombre de la etapa, en las reglas de su sistema. |
prioridade | string | baixa, normal, alta o urgente. |
origem | string | Cómo se abrió: manual (panel), chat (a partir de una conversación), ia, copiloto o api. |
conversa_id | entero o null | Conversación de origen, cuando la hay. |
telefone | string | Teléfono del cliente; vacío cuando no se informó. |
nome_cliente | string | Nombre del cliente; vacío cuando no se informó. |
departamento_id | entero o null | Departamento de la OS. |
responsavel_id | entero o null | Responsable; null sin dueño. |
responsavel | string | Nombre del responsable; vacío sin dueño. |
prazo_em | string o null | Plazo en ISO 8601, cuando está definido. |
ticket_atualizado agrega:
| Campo | Descripción |
|---|---|
mudanca | Lo que cambió: etapa (cambió de etapa), reaberto (salió de una etapa final hacia una de trabajo), responsavel (asignado, cambiado o retirado) o comentario. |
comentario | Solo cuando mudanca es comentario: el texto, con hasta 2000 caracteres. Los comentarios hechos por la API comienzan con [API: nombre de la clave]. |
{
"evento": "ticket_criado",
"emitido_em": "2026-10-01T09:25:40-03:00",
"dados": {
"ticket_id": 123,
"numero": "OS-000123",
"titulo": "Sin señal en la ONU: visita técnica",
"etapa_id": 1,
"etapa": "Nuevo",
"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": "Sin señal en la ONU: visita técnica",
"etapa_id": 3,
"etapa": "En 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 abierta, técnico en camino."
}
}Cuando un usuario del panel mueve una OS sin dueño a una etapa de trabajo, pasa a ser el responsable automáticamente. En ese caso sale un único ticket_atualizado con mudanca etapa, ya con el responsavel completado.
Firma HMAC
Cualquier persona que descubra la dirección de su webhook puede intentar enviar POST falsos. La firma garantiza que la entrega vino de su ChatISP y que el cuerpo no fue alterado:
X-ChatISP-Assinatura = "sha256=" + hex( HMAC_SHA256( clave = secreto, mensaje = cuerpo_crudo ) )
- Secreto: el valor completo mostrado en la pantalla, incluido el prefijo
whsec_. - Cuerpo crudo: los bytes exactos recibidos, antes de cualquier decodificación. No decodifique y vuelva a codificar el JSON para calcularla: la nueva serialización cambia espacios, orden o escapes (ChatISP envía acentos y barras sin escape) y la firma deja de coincidir.
- Compare en tiempo constante (
hash_equalsen PHP,crypto.timingSafeEqualen Node.js,hmac.compare_digesten Python). - Firma ausente o diferente: responda
401y descarte.
<?php $segredo = getenv('CHATISP_WEBHOOK_SEGREDO'); // whsec_... $corpo = file_get_contents('php://input'); // cuerpo CRUDO $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')); // clave única en "entrega": la repetición de una entrega ya guardada se ignora $pdo->prepare('INSERT IGNORE INTO chatisp_eventos (entrega, evento, corpo) VALUES (?, ?, ?)') ->execute([$entrega, $evento['evento'], $corpo]); } http_response_code(200); // responde ya; un proceso aparte trata la tabla
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 mantiene el cuerpo como Buffer, sin 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 procesar 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 crudos, sin 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) # su cola (Redis, RQ, Celery, tabla...) return "", 200
Hay más receptores completos en Ejemplos prácticos.
Otras protecciones recomendadas
- Use HTTPS en la dirección del webhook.
- Use una ruta difícil de adivinar en la URL (por ejemplo
/hooks/chatisp-8f2k1q) como capa adicional, nunca como sustituto de la firma. - Para descartar entregas antiguas reenviadas por terceros, compare
emitido_emcon el reloj de su servidor, teniendo en cuenta que los nuevos intentos legítimos pueden llegar hasta unas 7 horas y media después del evento. - Si su firewall solo permite orígenes conocidos, habilite la dirección IP de salida del servidor de ChatISP (en una instalación propia, su servidor; en ChatISP Cloud, consulte al soporte de Ravi Systems).
Reenvío y plazos
Cuando una entrega falla, ChatISP programa un nuevo intento con espera creciente. Son hasta 6 intentos en total:
| Intento | Espera después de la falla anterior | Tiempo aproximado desde el evento |
|---|---|---|
| 1.º | (primera pasada de la cola) | segundos |
| 2.º | 1 minuto | 1 minuto |
| 3.º | 5 minutos | 6 minutos |
| 4.º | 15 minutos | 21 minutos |
| 5.º | 1 hora | 1 hora y 21 minutos |
| 6.º y último | 6 horas | 7 horas y 21 minutos |
Los tiempos pueden extenderse un poco, porque la cola se revisa aproximadamente cada 20 segundos y cada pasada entrega hasta 100 eventos. Si el 6.º intento también falla, la entrega se abandona y no vuelve más.
- Caída larga = eventos perdidos. Si su sistema queda fuera de servicio por más de unas 7 horas, las entregas de ese período se agotan. Después de una caída larga, concilie por la API:
conversas/listarconatualizadas_desdeytickets/listartraen lo que cambió. - Mantenimiento planificado: pause el webhook (interruptor Activo desactivado). Las entregas pendientes quedan congeladas sin gastar intentos y se reanudan al reactivar. Los eventos que ocurran durante la pausa no se encolan; concilie ese intervalo por la API.
- Siga la cola: el número destacado en la columna Último envío muestra cuántas entregas esperan. Un número en aumento es señal de que su sistema está rechazando o está fuera de servicio.
Idempotencia y orden
Duplicados
Una entrega puede llegar más de una vez. Ocurre cuando su sistema procesó el evento, pero la respuesta no llegó a tiempo (por ejemplo, respondió después de 10 segundos, o la conexión se cayó antes de la respuesta): ChatISP lo considera una falla e intenta de nuevo.
Use el encabezado X-ChatISP-Entrega como clave de idempotencia:
- Es el mismo en todos los intentos de la misma entrega, y el cuerpo también es idéntico.
- Es único por webhook: el mismo evento entregado a dos webhooks tiene números de entrega diferentes.
- Guarde los números ya procesados (basta una tabla con clave única, como en el ejemplo en PHP de arriba) e ignore los repetidos, respondiendo 200.
Como segunda línea de defensa, use los identificadores del propio evento: mensagem.id para mensajes, conversa_id junto con el tipo de evento para conversaciones, ticket_id para órdenes de servicio.
Orden de llegada
En condiciones normales, las entregas salen aproximadamente en el orden de los eventos (por ejemplo, conversa_criada antes del mensagem_recebida del primer mensaje). Pero no hay garantía: eventos del mismo segundo pueden salir en otro orden, y una entrega que falla y pasa a un nuevo intento es superada por las siguientes. Reglas prácticas:
- Ordene por
emitido_emy por los ids, nunca por el orden de llegada. - Para el estado actual de una conversación o una OS, no acumule los eventos: use el evento como disparador y consulte el estado en la API (
conversas/consultar,tickets/consultar). - En un
mensagem_status, solo avance el status de su lado (enviada,entregue,lida); nunca vuelva delidaaentreguepor un evento atrasado.
Probar y monitorear
Botón Probar
En la lista de webhooks, Probar hace en el momento una entrega del evento teste, fuera de la cola y sin nuevos intentos, con la misma firma y los mismos encabezados de las entregas reales (X-ChatISP-Entrega vale 0):
{"evento":"teste","emitido_em":"2026-10-01T08:30:00-03:00","dados":{"webhook_id":2}}El resultado aparece en la pantalla:
- "Entrega de prueba aceptada (http 200)." cuando su sistema respondió 2xx.
- "La entrega de prueba falló (...)." con el código HTTP o el error de conexión, por ejemplo
http 401(la firma no coincidió de su lado) o un mensaje de tiempo agotado o de nombre no encontrado.
La prueba funciona incluso con el webhook inactivo y actualiza la columna Último envío. Su sistema debe responder 2xx al evento teste y no hacer nada con él.
Probar con eventos reales
- Registre el webhook con los eventos deseados.
- Envíe un mensaje desde un celular suyo al número del proveedor: llegan
conversa_criada(si no había conversación abierta) ymensagem_recebida. - Responda desde el panel: llegan
conversa_atribuida(al tomar la conversación),mensagem_enviaday, a continuación,mensagem_statusconentregueylida. - Transfiera y cierre la conversación para ver
conversa_transferidayconversa_encerrada. Abra y mueva una OS para ver los eventos de orden de servicio.
Sin receptor listo
Para ver el formato real antes de programar, registre temporalmente la dirección de un servicio de inspección de solicitudes (del tipo "request bin") y haga clic en Probar. Después cámbiela por la dirección definitiva. No deje un servicio de terceros registrado en producción: las entregas reales llevan datos de sus clientes.
Eventos de volumen alto
mensagem_enviada y mensagem_status acompañan cada mensaje enviado por el proveedor, incluidos los de la IA, los flujos, las campañas y la regla de cobranza. Cada mensaje enviado genera un mensagem_enviada y normalmente uno o dos mensagem_status más (entregue y lida). Por eso vienen desmarcados en un webhook nuevo.
Capacidad de entrega
ChatISP entrega hasta 100 eventos por pasada de la cola, en paralelo, y la cola se recorre aproximadamente cada 20 segundos: algo del orden de 300 entregas por minuto, sumando todos los webhooks. Los picos por encima de eso no se pierden, pero forman cola y llegan con retraso. Ejemplo: una campaña de 1.000 mensajes con los dos eventos de volumen alto activados genera unas 3.000 entregas, que tardan alrededor de 10 minutos en salir; en ese intervalo, los eventos de conversación también esperan en la misma cola.
Recomendaciones
- Active
mensagem_enviadaymensagem_statussolo si su sistema realmente usa esa información (por ejemplo, para comprobar la entrega de un aviso de cobranza). - Separe en webhooks diferentes los eventos de volumen alto y los de conversación y orden de servicio, apuntando a direcciones diferentes. Cada webhook tiene sus propios intentos, y el sistema que trata conversaciones y órdenes de servicio no necesita recibir ni descartar miles de recibos.
- Para confirmar la entrega de un envío hecho por la propia API, también se puede consultar el
statusdel mensaje conconversas/mensagensen lugar de suscribirse a todos los recibos. - Responda en milisegundos: con volumen alto, un receptor lento acumula fallas por tiempo agotado y sus entregas vuelven para un nuevo intento, llegando con retraso.
Buenas prácticas y problemas frecuentes
Lista de verificación del receptor
- Validar la firma con el cuerpo crudo, en tiempo constante.
- Responder 2xx rápido, muy por debajo de 10 segundos; procesar en una cola propia, en segundo plano.
- Descartar duplicados por el
X-ChatISP-Entrega. - Tratar el evento
testecon 2xx y sin efecto. - Ignorar eventos y campos desconocidos, para seguir funcionando si ChatISP agrega información.
- No depender del orden de llegada; consultar la API cuando necesite el estado actual.
- Suscribirse solo a los eventos necesarios y separar los de volumen alto.
- Usar HTTPS con certificado válido.
Problemas frecuentes
| Síntoma | Causa probable y solución |
|---|---|
La prueba falla con http 401 o http 403 | Su receptor rechazó la entrega. Normalmente es una firma calculada sobre el JSON reserializado, o un secreto copiado sin el prefijo whsec_. |
La prueba falla con http 301 o http 302 | La dirección redirige (de http:// a https://, o por una barra al final). Las redirecciones no se siguen: registre la dirección final. |
| La prueba falla por tiempo agotado | Su sistema tardó más de 10 segundos, o el servidor de ChatISP no alcanza la dirección (firewall, red interna, DNS). |
| La prueba pasa, pero los eventos reales no llegan | Webhook inactivo, evento no marcado, o el caso probado no genera el evento (vea Lo que no genera evento). Recuerde que las entregas llegan algunos segundos después. |
| El mismo evento llegó dos veces | Comportamiento esperado tras una falla o una respuesta lenta. Descarte por el X-ChatISP-Entrega. |
| Eventos llegando con minutos de retraso | Cola acumulada por eventos de volumen alto activados durante una campaña, o entregas en nuevo intento porque el receptor tardó más de 10 segundos. Vea Eventos de volumen alto. |
| El número de entregas en cola no deja de crecer | Su sistema está rechazando o está fuera de servicio. Vea el resultado en Último envío, corrija, y los próximos intentos entregan lo acumulado. |