Los webhooks son callbacks HTTP que se configuran para cada cuenta. Estos se activan cuando ocurren acciones como la creación de un mensaje en Chatwoot. Se pueden crear múltiples webhooks para una sola cuenta.
¿Cómo agregar un webhook?
Paso 1. Ve a Configuraciones → Integraciones → Webhooks. Haz clic en el botón "Configurar".

Paso 2. Haz clic en el botón "Agregar nuevo webhook". Se abrirá un modal. Aquí, ingresa la URL a la que se debe enviar la solicitud POST. Luego, debes seleccionar los eventos a los que deseas suscribirte. Esta opción te permitirá escuchar solo los eventos relevantes en Chatwoot.

Chatwoot enviará una solicitud POST con el siguiente payload a las URLs configuradas para diversas actualizaciones en tu cuenta.
Ejemplo de payload de un webhook
{
"event": "message_created", // El nombre del evento
"id": "1", // ID del mensaje
"content": "Hi", // Contenido del mensaje
"created_at": "2020-03-03 13:05:57 UTC", // Hora en la que se envió el mensaje
"message_type": "incoming", // Esto puede ser incoming, outgoing o template. El usuario envía mensajes incoming desde el widget, y el agente envía mensajes outgoing al usuario.
"content_type": "enum", // Esto es un enum y puede ser input_select, cards, form o text. message_type será template si content_type es alguno de estos. El valor por defecto es text
"content_attributes": {} // Este será un objeto, los diferentes valores se definen abajo
"source_id": "", // Este será el id externo si la bandeja es una integración de Twitter o Facebook.
"sender": { // Esto proveerá los detalles del agente que envió el mensaje
"id": "1",
"name": "Agent",
"email": "agent@example.com"
},
"contact": { // Esto proveerá los detalles del usuario que envió el mensaje
"id": "1",
"name": "contact-name"
},
"conversation": { // Esto proveerá los detalles de la conversación
"display_id": "1", // Este es el ID de la conversación que puedes ver en el dashboard.
"additional_attributes": {
"browser": {
"device_name": "Macbook",
"browser_name": "Chrome",
"platform_name": "Macintosh",
"browser_version": "80.0.3987.122",
"platform_version": "10.15.2"
},
"referer": "<http://www.chatwoot.com>",
"initiated_at": "Tue Mar 03 2020 18:37:38 GMT-0700 (Mountain Standard Time)"
}
},
"account": { // Esto proveerá los detalles de la cuenta
"id": "1",
"name": "Chatwoot",
}
}
Eventos de webhook soportados en Chatwoot
Chatwoot publica diversos eventos a los endpoints de webhook configurados. Si deseas configurar un webhook, consulta la guía aquí.
Cada evento tiene su propia estructura de payload basada en el tipo de modelo sobre el que actúan. La siguiente sección describe los principales objetos que usamos en Chatwoot y sus atributos.
Objetos
Un payload de evento puede incluir cualquiera de los siguientes objetos. Los diferentes tipos de objetos soportados por Chatwoot se enumeran a continuación.
Cuenta
{
"id": "integer",
"name": "string"
}
Bandeja de entrada
{
"id": "integer",
"name": "string"
}
Contacto
{
"id": "integer",
"name": "string",
"avatar": "string",
"type": "contact",
"account": {
// <...Objeto Cuenta>
}
}
Usuario
{
"id": "integer",
"name": "string",
"email": "string",
"type": "user"
}
Conversación
{
"additional_attributes": {
"browser": {
"device_name": "string",
"browser_name": "string",
"platform_name": "string",
"browser_version": "string",
"platform_version": "string"
},
"referer": "string",
"initiated_at": {
"timestamp": "iso-datetime"
}
},
"can_reply": "boolean",
"channel": "string",
"id": "integer",
"inbox_id": "integer",
"contact_inbox": {
"id": "integer",
"contact_id": "integer",
"inbox_id": "integer",
"source_id": "string",
"created_at": "datetime",
"updated_at": "datetime",
"hmac_verified": "boolean"
},
"messages": ["Array de objetos mensaje"],
"meta": {
"sender": {
// Objeto Contacto
},
"assignee": {
// Objeto Usuario
}
},
"status": "string",
"unread_count": "integer",
"agent_last_seen_at": "unix-timestamp",
"contact_last_seen_at": "unix-timestamp",
"timestamp": "unix-timestamp",
"account_id": "integer"
}
Mensaje
{
"id": "integer",
"content": "string",
"message_type": "integer",
"created_at": "unix-timestamp",
"private": "boolean",
"source_id": "string / null",
"content_type": "string",
"content_attributes": "object",
"sender": {
"type": "string - contact/user"
// Objeto Usuario o Contacto
},
"account": {
// Objeto Cuenta
},
"conversation": {
// Objeto Conversación
},
"inbox": {
// Objeto Bandeja de entrada
}
}
Ejemplo de payload de un webhook
{
"event": "event_name"
// Atributos relacionados con el evento
}
Eventos de Webhook
Chatwoot soporta los siguientes eventos de webhook. Puedes suscribirte a ellos mientras configuras un webhook en el dashboard o usando la API.
conversation_created
Este evento se activará cuando se cree una nueva conversación en la cuenta. El payload del evento es el siguiente.
{
"event": "conversation_created"
// <...Atributos de Conversación>
}
conversation_updated
Este evento se activará cuando haya un cambio en alguno de los atributos de la conversación.
{
"event": "conversation_updated",
"changed_attributes": [
{
"<attribute_name>": {
"current_value": "",
"previous_value": ""
}
}
]
// <...Atributos de Conversación>
}
conversation_status_changed
Este evento se activará cuando el estado de la conversación cambie.
Nota: Si estás utilizando APIs de agent bot en vez de webhooks, este evento aún no es soportado.
{
"event": "conversation_status_changed"
// <...Atributos de Conversación>
}
message_created
Este evento se activará cuando se cree un mensaje en una conversación. El payload del evento es el siguiente.
{
"event": "message_created"
// <...Atributos de Mensaje>
}
message_updated
Este evento se activará cuando un mensaje sea actualizado en una conversación. El payload del evento es el siguiente.
{
"event": "message_updated"
// <...Atributos de Mensaje>
}
webwidget_triggered
Este evento se activa cuando el usuario final abre el widget de chat en vivo.
{
"event": "webwidget_triggered",
"id": "",
"contact": {
// <...Objeto Contacto>
},
"inbox": {
// <...Objeto Bandeja de entrada>
},
"account": {
// <...Objeto Cuenta>
},
"current_conversation": {
// <...Objeto Conversación>
},
"source_id": "string",
"event_info": {
"initiated_at": {
"timestamp": "date-string"
},
"referer": "string",
"widget_language": "string",
"browser_language": "string",
"browser": {
"browser_name": "string",
"browser_version": "string",
"device_name": "string",
"platform_name": "string",
"platform_version": "string"
}
}
}
conversation_typing_on
Este evento se activa cuando un agente comienza a escribir en una conversación. Puede ser una nota privada o un mensaje al cliente. Puedes usar el flag is_private para distinguir entre los dos.
{
"event": "conversation_typing_on",
"conversation": { ...<Objeto Conversación> },
"user": { ... <Objeto Usuario / AgentBot / Captain> },
"is_private": true
}
conversation_typing_off
Este evento se activa cuando un agente deja de escribir o cuando abandona la ventana de conversación.
{
"event": "conversation_typing_off",
"conversation": { ...<Objeto Conversación> },
"user": { ... <Objeto Usuario / AgentBot / Captain> },
"is_private": true
}
Verificación de webhooks
Chatwoot firma cada solicitud de webhook saliente para que tu servidor pueda verificar que el payload fue enviado por Chatwoot y que no ha sido alterado. El secreto se muestra una vez que el webhook es creado y puedes verlo nuevamente en el formulario de edición del webhook.
Cada solicitud de webhook envía los siguientes encabezados, que se pueden utilizar para calcular la firma HMAC del payload
-
X-Chatwoot-Signature: Firma HMAC-SHA256 con prefijosha256= -
X-Chatwoot-Timestamp: Marca de tiempo Unix (segundos) cuando la solicitud fue firmada -
X-Chatwoot-Delivery: ID único de entrega para el evento webhook (cuando esté disponible)
La firma se calcula así:
sha256=HMAC-SHA256(webhook_secret, "{timestamp}.{raw_body}")
Donde:
-
webhook_secretes el secreto asociado al webhook -
timestampes el valor del encabezadoX-Chatwoot-Timestamp -
raw_bodyes el cuerpo bruto de la solicitud JSON (sin parsear/serializar nuevamente)
Pasos de verificación
-
Extrae
X-Chatwoot-SignatureyX-Chatwoot-Timestampde los encabezados de la solicitud -
Lee el cuerpo crudo de la solicitud como bytes (no lo parsees ni serialices de nuevo)
-
Calcula la firma esperada:
sha256=HMAC-SHA256(secret, "{timestamp}.{raw_body}") -
Compara la firma calculada con la firma recibida usando una comparación en tiempo constante
-
Opcionalmente, rechaza las solicitudes donde la marca de tiempo es demasiado antigua para evitar ataques de replay
Ejemplos
Ruby
def verify_signature(raw_body, timestamp, received_signature, secret)
expected = "sha256=#{OpenSSL::HMAC.hexdigest('SHA256', secret, "#{timestamp}.#{raw_body}")}"
ActiveSupport::SecurityUtils.secure_compare(expected, received_signature)
end
Python
import hmac
import hashlib
def verify_signature(raw_body: bytes, timestamp: str, received_signature: str, secret: str) -> bool:
message = f"{timestamp}.".encode() + raw_body
expected = "sha256=" + hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received_signature)
Node.js
const crypto = require("crypto");
function verifySignature(rawBody, timestamp, receivedSignature, secret) {
const message = `${timestamp}.${rawBody}`;
const expected =
"sha256=" + crypto.createHmac("sha256", secret).update(message).digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(receivedSignature)
);
}
Go
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
)
func verifySignature(rawBody []byte, timestamp, receivedSignature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(fmt.Sprintf("%s.%s", timestamp, rawBody)))
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(receivedSignature))
}
Notas importantes
-
Siempre usa el cuerpo crudo de la solicitud para la verificación. Parsear el JSON y volver a serializarlo puede cambiar el orden de las claves, los espacios en blanco o el escape de unicode, lo cual producirá una firma diferente.
-
Siempre usa una comparación en tiempo constante (por ejemplo,
hmac.compare_digest,crypto.timingSafeEqual,ActiveSupport::SecurityUtils.secure_compare) para evitar ataques de temporización. -
Considera rechazar solicitudes con marcas de tiempo mayores a 5 minutos para mitigar ataques de replay.