Webhooks

Los webhooks conectan Yolani con tu infraestructura en tiempo real, en ambas direcciones:

  • 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

  1. En el Dashboard ve a Webhooks
  2. Registra la URL de tu servidor (o de tu nodo Webhook en n8n/Make)
  3. Elige los eventos que quieres recibir (o * para todos)
  4. Opcional: limita el webhook a ciertos canales (WhatsApp, Instagram, Facebook, chat web)
  5. Guarda el secret que se genera: lo necesitas para verificar las firmas
  6. Usa el botón Enviar prueba para mandar un evento test.ping

Eventos disponibles

EventoSe dispara cuando
message.receivedUn cliente te envía un mensaje (cualquier canal)
message.sentSe envía un mensaje desde tu cuenta (humano, IA o API)
conversation.createdSe abre una conversación nueva
conversation.updatedCambia el estado de una conversación
conversation.closedUna conversación se marca como resuelta
conversation.bot.enabledSe activa el agente IA en una conversación
conversation.bot.disabledSe pausa el agente IA (handoff a humano)
conversation.stage_changedUn cliente pasa a otra etapa de tu embudo
contact.createdAlguien te escribe por primera vez, o das de alta un contacto en el Dashboard o por la API
contact.updatedCambian los datos de un contacto (Dashboard, API, o el nombre y correo que le dio a tu agente)
appointment.createdSe agenda una cita (tu agente, el Dashboard, tu enlace de reservas, Calendly o Google Calendar)
appointment.rescheduledUna cita cambia de horario o de agenda
appointment.cancelledUna cita se cancela
test.pingPrueba 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 channel en su payload: message.received, message.sent, conversation.created, conversation.stage_changed y los de contactos.
  • Los eventos sin canal (conversation.updated, conversation.closed, los de agente IA, los de citas y test.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:

HeaderDescripción
X-Yolani-EventTipo de evento
X-Yolani-Event-IdID único del evento (para deduplicar)
X-Yolani-SignatureFirma HMAC SHA-256 del body
X-Yolani-AttemptNú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

  1. En n8n crea un workflow con un nodo Webhook (método POST)
  2. Copia la URL de producción del nodo y regístrala en Yolani > Webhooks
  3. Suscríbete a los eventos que necesites (ej. message.received)
  4. Para responder mensajes desde n8n, agrega un nodo HTTP Request apuntando a POST https://api.yolani.co/api/v1/conversations/{{id}}/messages con tu API key en el header Authorization

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.