- Salientes: Yolani envía eventos a tu URL (mensaje recibido, conversación creada...)
- Entrantes: tu sistema dispara acciones en Yolani (crear contacto, agregar etiqueta...)
Webhooks salientes
Configuración
- En el Dashboard ve a Webhooks
- Registra la URL de tu servidor (o de tu nodo Webhook en n8n/Make)
- Elige los eventos que quieres recibir (o
*para todos) - Opcional: limita el webhook a ciertos canales (WhatsApp, Instagram, Facebook, chat web)
- Guarda el secret que se genera: lo necesitas para verificar las firmas
- Usa el botón Enviar prueba para mandar un evento
test.ping
Eventos disponibles
| Evento | Se dispara cuando |
|---|---|
message.received | Un cliente te envía un mensaje (cualquier canal) |
message.sent | Se envía un mensaje desde tu cuenta (humano, IA o API) |
conversation.created | Se abre una conversación nueva |
conversation.updated | Cambia el estado de una conversación |
conversation.closed | Una conversación se marca como resuelta |
conversation.bot.enabled | Se activa el agente IA en una conversación |
conversation.bot.disabled | Se pausa el agente IA (handoff a humano) |
conversation.stage_changed | Un cliente pasa a otra etapa de tu embudo |
contact.created | Alguien te escribe por primera vez, o das de alta un contacto en el Dashboard o por la API |
contact.updated | Cambian los datos de un contacto (Dashboard, API, o el nombre y correo que le dio a tu agente) |
appointment.created | Se agenda una cita (tu agente, el Dashboard, tu enlace de reservas, Calendly o Google Calendar) |
appointment.rescheduled | Una cita cambia de horario o de agenda |
appointment.cancelled | Una cita se cancela |
test.ping | Prueba manual desde el Dashboard |
También puedes suscribirte con comodines: message.*, conversation.*, contact.* o appointment.*.
Contactos nuevos:
Los contactos que importas (CSV, historial de WhatsApp) no disparan contact.created. Cuando una persona escribe por primera vez, contact.created llega antes que conversation.created, pero la entrega no garantiza el orden: usa contact_id para relacionarlos.
Filtro por canal
Cada webhook puede limitarse a uno o más canales: whatsapp, instagram, facebook y web_widget (chat web). Por ejemplo, puedes mandar solo los mensajes de WhatsApp a un flujo de n8n y los de Instagram a otro.
- Sin filtro (default), el webhook recibe eventos de todos los canales.
- El filtro aplica a los eventos que traen
channelen su payload:message.received,message.sent,conversation.created,conversation.stage_changedy los de contactos. - Los eventos sin canal (
conversation.updated,conversation.closed, los de agente IA, los de citas ytest.ping) se entregan siempre.
Formato del payload
Cada evento llega como POST con JSON:
{
"event_id": "3fa4c7de-9f11-4c0a-9d3e-2b8f0a61c111",
"event_type": "message.received",
"occurred_at": "2026-07-14T15:03:22.481Z",
"data": {
"message_id": 15204,
"conversation_id": 913,
"contact_id": 481,
"direction": "inbound",
"channel": "whatsapp",
"type": "text",
"content_preview": "¿Tienen envío a domicilio?",
"has_media": false,
"sent_by_bot": false,
"created_at": "2026-07-14T15:03:22.000Z"
}
}
Ejemplos de data de los eventos de CRM:
// contact.created
{
"contact_id": 481,
"name": "Lucía Pérez",
"phone": "5215512345678",
"email": null,
"channel": "whatsapp",
"instagram_id": null,
"facebook_id": null,
"tags": [],
"created_at": "2026-10-06T18:02:11.000Z",
"updated_at": "2026-10-06T18:02:11.000Z",
"source": "conversation",
"conversation_id": 913
}
// appointment.created
{
"appointment_id": 8525,
"status": "confirmed",
"title": "Limpieza dental",
"start_time": "2026-10-08T17:00:00.000Z",
"end_time": "2026-10-08T18:00:00.000Z",
"duration_minutes": 60,
"resource_id": 3,
"resource_name": "Dra. Ana Licea",
"service_id": 12,
"service_name": "Limpieza dental",
"contact_id": 481,
"conversation_id": 913,
"customer_name": "Lucía Pérez",
"customer_phone": "5215512345678",
"source": "agente",
"google_event_id": null,
"replaced_by_appointment_id": null
}
// conversation.stage_changed
{
"conversation_id": 913,
"contact_id": 481,
"channel": "whatsapp",
"pipeline_id": 2,
"pipeline_name": "Ventas",
"stage_id": 12,
"stage_name": "Cita agendada",
"previous_stage_id": 11,
"previous_stage_name": "Interesado",
"source": "ai",
"actor_user_id": null
}
source dice quién lo hizo. En contactos: conversation, panel, api o agent; contact.updated trae además changed_fields. En etapas: manual, ai, system o pipeline_assignment. En citas: agente, panel, enlace, google, calendly, voz y otros; puede venir null.
Citas: agenda, servicio y reagendados:
resource_id y resource_name son la agenda de la cita (por ejemplo, un médico); service_id y service_name, el servicio. Si la cita no tiene servicio vienen null. Cuando reagendas desde el Dashboard, appointment.rescheduled trae la misma appointment_id con el horario nuevo. Cuando la reagenda tu agente, recibes appointment.created de la cita nueva y appointment.rescheduled de la anterior, con su horario anterior, status: "rescheduled" y replaced_by_appointment_id igual a la appointment_id de la nueva: da de baja la primera y guarda la segunda. Cancelar una cita, desde donde sea, manda appointment.cancelled; si la vuelves a activar, llega otra vez como appointment.created con la misma appointment_id. Marcarla como completada o como "no asistió" no manda aviso.
Headers incluidos:
| Header | Descripción |
|---|---|
X-Yolani-Event | Tipo de evento |
X-Yolani-Event-Id | ID único del evento (para deduplicar) |
X-Yolani-Signature | Firma HMAC SHA-256 del body |
X-Yolani-Attempt | Número de intento (1 a 6) |
Verificar la firma
Calcula el HMAC SHA-256 del body crudo con tu secret y compáralo con X-Yolani-Signature (formato sha256=<hex>):
const crypto = require('crypto');
function verifyYolaniSignature(rawBody, signatureHeader, secret) {
const expected =
'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(expected),
);
}
import hmac, hashlib
def verify_yolani_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature_header, expected)
Reintentos
Tu endpoint debe responder 2xx en menos de 8 segundos. Si falla, Yolani reintenta con espera creciente: 30s, 2min, 10min, 1h, 6h y 24h. Tras 6 intentos fallidos el evento se descarta y recibes una notificación en el Dashboard.
Responde rápido:
Responde 200 de inmediato y procesa el evento de forma asíncrona. Usa X-Yolani-Event-Id para descartar duplicados.
Uso con n8n
- En n8n crea un workflow con un nodo Webhook (método POST)
- Copia la URL de producción del nodo y regístrala en Yolani > Webhooks
- Suscríbete a los eventos que necesites (ej.
message.received) - Para responder mensajes desde n8n, agrega un nodo HTTP Request apuntando a
POST https://api.yolani.co/api/v1/conversations/{{id}}/messagescon tu API key en el headerAuthorization
Webhooks entrantes
Permiten que un sistema externo dispare acciones en Yolani sin usar la API completa.
Configuración
Cada endpoint entrante tiene una URL única con token:
POST https://api.yolani.co/api/webhooks/inbound/{tu-slug-token}
El body debe ir firmado con HMAC SHA-256 (header X-Yolani-Signature: sha256=<hex>) usando el secret del endpoint.
Acciones soportadas
{ "event": "contact.upsert", "data": { "phone": "5215512345678", "name": "Juan", "email": "juan@ejemplo.com" } }
{ "event": "contact.tag.add", "data": { "phone": "5215512345678", "tag": "cliente-vip" } }
{ "event": "contact.tag.remove", "data": { "phone": "5215512345678", "tag": "prospecto" } }
¿Necesitas más acciones?:
Para enviar mensajes o gestionar conversaciones usa la API v1, que cubre esos casos con más control y permisos granulares.