Webhooks salientes
Recibir en tu sistema los eventos que ocurren dentro de Kelova
Kelova puede avisarle a tu sistema cuando pasa algo: una reserva confirmada por el agente, un lead capturado en una conversación. Registras una URL HTTPS y Kelova le hace un POST firmado con el evento.
Es la vía para conectar Kelova con n8n, Make, Zapier o tu propio backend sin consultar la API cada minuto.
Los webhooks salientes requieren un plan de pago (Piloto en adelante) y rol admin. No confundir con Webhooks, que son los entrantes de WhatsApp y Telegram hacia Kelova.
Eventos disponibles
| Evento | Cuándo se dispara |
|---|---|
booking.created | El agente confirmó una cita con un cliente |
lead.created | El agente capturó un lead en una conversación |
test.ping | Solo a pedido, con el endpoint de prueba. No se suscribe |
Un endpoint recibe únicamente los eventos a los que está suscrito — salvo test.ping, que llega siempre que lo dispares a mano.
Crear un endpoint
El secreto de firma se genera acá y se muestra una sola vez. Kelova lo guarda cifrado y no existe forma de volver a verlo: si lo pierdes, rotas.
curl -X POST https://api.kelova.app/integrations/webhooks \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://n8n.tuempresa.com/webhook/kelova",
"events": ["booking.created", "lead.created"]
}'Respuesta (201):
{
"endpoint": {
"id": "9c1e2b4a...",
"url": "https://n8n.tuempresa.com/webhook/kelova",
"events": ["booking.created", "lead.created"],
"status": "active",
"consecutive_failures": 0,
"last_triggered_at": null,
"created_at": "2026-09-05T14:00:00Z",
"updated_at": "2026-09-05T14:00:00Z"
},
"secret": "whsec_3f9a1c7e5b2d8046a1c3e5f7b9d0..."
}Copia secret de inmediato. Ninguna otra respuesta lo devuelve — ni en claro ni cifrado.
Restricciones de la URL:
- Tiene que ser
https://. HTTP plano se rechaza conINVALID_WEBHOOK_URL. - No puede apuntar a una red interna, a
localhostni a rangos especiales. Kelova resuelve el nombre y valida cada IP antes de conectar, y vuelve a validar en cada redirección (máximo 3). - Hasta 5 endpoints por tenant.
Qué recibe tu servidor
POST /webhook/kelova HTTP/1.1
Content-Type: application/json
User-Agent: Kelova-Webhooks/1
X-Kelova-Event: booking.created
X-Kelova-Delivery: 7d3f9a1c-5b2d-4046-a1c3-e5f7b9d02468
X-Kelova-Signature: t=1788901678,v1=5f3c...{
"id": "7d3f9a1c-5b2d-4046-a1c3-e5f7b9d02468",
"type": "booking.created",
"tenant_id": "t9f8e7...",
"created_at": "2026-09-05T14:32:10Z",
"data": {
"booking_id": "b1a2c3...",
"agent_id": "a4d5e6...",
"conversation_id": "c7f8a9...",
"starts_at": "2026-09-08T15:00:00Z",
"duration_min": 30,
"timezone": "America/El_Salvador",
"contact_name": "María Rivera",
"contact_email": "maria@ejemplo.com",
"contact_phone": "+50370001234"
}
}starts_at viaja siempre en UTC; timezone es la zona de la cuenta, la que el cliente vio al agendar.
El data de lead.created:
{
"session_id": "s1b2c3...",
"agent_id": "a4d5e6...",
"channel": "whatsapp",
"intent": "cotización",
"captured_at": "2026-09-05T14:32:10Z",
"contact_name": "María Rivera",
"contact_email": "maria@ejemplo.com",
"contact_phone": "+50370001234"
}Los campos de contacto se omiten cuando el cliente no los dio. Responde cualquier 2xx para aceptar la entrega; Kelova espera hasta 10 segundos.
Verificar la firma
X-Kelova-Signature tiene el formato t=<unix>,v1=<hex>, donde v1 es HMAC-SHA256 del secreto sobre la cadena <t>.<cuerpo exacto>. El timestamp entra en el material firmado, así que no se puede reusar una firma vieja con un t nuevo.
const crypto = require("crypto");
function verificar(secreto, header, cuerpoCrudo) {
const partes = Object.fromEntries(
header.split(",").map((p) => p.split("=", 2))
);
const t = Number(partes.t);
// Rechaza lo que esté a más de 5 minutos de tu reloj.
if (Math.abs(Date.now() / 1000 - t) > 300) return false;
const esperado = crypto
.createHmac("sha256", secreto)
.update(`${t}.${cuerpoCrudo}`)
.digest("hex");
// Puede venir más de un v1= durante una rotación: basta con que uno coincida.
return header
.split(",")
.filter((p) => p.startsWith("v1="))
.some((p) =>
crypto.timingSafeEqual(
Buffer.from(p.slice(3), "hex"),
Buffer.from(esperado, "hex")
)
);
}Firma sobre el cuerpo crudo, byte por byte. Si tu framework parsea el JSON y lo vuelve a serializar, la firma no va a coincidir.
Reintentos y entregas duplicadas
Si tu servidor no responde 2xx, Kelova reintenta cuatro veces: 1 minuto, 5 minutos, 25 minutos y 2 horas después del intento en línea. Agotada la escalera, la entrega queda en failed.
Cada reintento vuelve a firmar (el t cambia) pero conserva el mismo X-Kelova-Delivery. Deduplica por ese header, no por el contenido.
Una URL que apunta a una red interna no se reintenta: no va a dejar de hacerlo por esperar.
Tras 10 fallos consecutivos el endpoint se desactiva solo y recibes una notificación en el dashboard. Se reactiva con un PATCH que lo ponga en active, lo que además pone el contador de fallos en cero.
Rotar el secreto
curl -X POST https://api.kelova.app/integrations/webhooks/{id}/rotate-secret \
-H "Authorization: Bearer <token>"{
"secret": "whsec_8b0d2f4a6c8e...",
"grace_period_ends_in": "1h0m0s"
}Durante una hora Kelova firma con el secreto nuevo y con el anterior: el header trae dos v1= y tu receptor sigue validando con el que todavía tenga configurado. Pasada la gracia, solo vale el nuevo.
Probar un endpoint
Dispara un test.ping contra un endpoint concreto, esté suscrito a lo que esté:
curl -X POST https://api.kelova.app/integrations/webhooks/{id}/test \
-H "Authorization: Bearer <token>"Responde 202 — la entrega es asíncrona. El resultado se consulta en el registro de entregas. Máximo una prueba por minuto por endpoint; un endpoint desactivado responde 409.
Registro de entregas
curl "https://api.kelova.app/integrations/webhooks/deliveries?status=failed&limit=20" \
-H "Authorization: Bearer <token>"{
"deliveries": [
{
"id": "7d3f9a1c...",
"endpoint_id": "9c1e2b4a...",
"event_type": "booking.created",
"status": "failed",
"attempts": 5,
"next_retry_at": null,
"response_status": 500,
"response_body": "internal error",
"duration_ms": 412,
"delivered_at": null,
"created_at": "2026-09-05T14:32:10Z",
"payload": { "booking_id": "b1a2c3..." }
}
]
}Filtros: status (pending, success, failed), endpoint_id y limit (default 50, tope 200). Se guardan los primeros 1 KB de la respuesta de tu servidor, útil para depurar. Las entregas se retienen 90 días.
Listar, editar y borrar
# Listar — nunca devuelve el secreto
curl https://api.kelova.app/integrations/webhooks \
-H "Authorization: Bearer <token>"
# Cambiar URL, eventos o estado
curl -X PATCH https://api.kelova.app/integrations/webhooks/{id} \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"status": "disabled"}'
# Borrar el endpoint y sus entregas
curl -X DELETE https://api.kelova.app/integrations/webhooks/{id} \
-H "Authorization: Bearer <token>"Desactivar y editar la URL funcionan siempre, incluso si tu plan bajó — para que puedas frenar tus webhooks en cualquier momento. Reactivar sí vuelve a pedir plan de pago.
Errores
| Código | HTTP | Cuándo ocurre |
|---|---|---|
WEBHOOKS_REQUIRE_PAID_PLAN | 403 | Tu plan no incluye webhooks salientes |
WEBHOOK_LIMIT_REACHED | 409 | Ya tienes 5 endpoints |
WEBHOOK_NOT_FOUND | 404 | El endpoint no existe o no es de tu tenant |
INVALID_WEBHOOK_URL | 400 | No es HTTPS, está malformada o apunta a una red no permitida |
INVALID_WEBHOOK_EVENTS | 400 | Lista vacía, evento repetido o fuera del catálogo |
Todavía no
Configurar webhooks desde el dashboard (por ahora es solo API), los eventos handoff.requested y order.confirmed, y el reenvío manual de una entrega fallida.