Webhooks
En lugar de hacer polling, suscríbete a eventos salientes: Kinmu envía un POST a tu endpoint HTTPS cuando ocurre algo relevante. Cada entrega va firmada con la convención Standard Webhooks — verifica siempre la firma antes de procesar el evento.
La gestión de endpoints requiere el scope webhooks:manage y se hace desde el dashboard o vía los endpoints /v1/webhook-endpoints.
Suscripción
Crea un endpoint indicando tu URL (HTTPS) y los eventos que te interesan:
curl -s -X POST "https://api.kinmu.app/v1/webhook-endpoints" \
-H "Authorization: Bearer kinmu_sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"url": "https://tu-app.example.com/webhooks/kinmu",
"events": ["absence.approved", "checkin.created"],
"description": "Sync con nuestro ERP"
}'La respuesta incluye el secreto de firma (whsec_…), visible una sola vez. Guárdalo: lo necesitas para verificar las firmas.
Gestión disponible: GET (lista), GET /{id}, PATCH /{id} (eventos/estado), DELETE /{id}, POST /{id}/test (envía un ping), GET /{id}/deliveries (últimas entregas) y POST /{id}/deliveries/{deliveryId}/retry (reencola una entrega concreta). Máximo 5 endpoints activos por empresa.
Eventos
| Evento | Cuándo se emite | data |
|---|---|---|
employee.created · employee.updated · employee.terminated | Alta, cambio o baja de un empleado. | id y status. |
checkin.created | Se registra un fichaje (entrada, salida, inicio o fin de pausa). | id del evento ({uuid}:{tipo}) y employee_id. |
absence.requested | Se crea una solicitud pendiente. | id, employee_id, type y status. |
absence.approved · absence.denied · absence.cancelled | Se decide o cancela una ausencia. Importar una ausencia ya aprobada emite absence.approved, sin absence.requested previo. | id, employee_id, type y status. |
vacation_balance.updated | Se aprueban o cancelan unas vacaciones. | Igual que una fila de GET /v1/vacation-balances. |
report.completed | Termina la generación de un informe. | id y status; pide GET /v1/reports/{id} para la download_url. |
ping | Lo envías tú con POST /v1/webhook-endpoints/{id}/test. | { "message": "pong" }. |
Los eventos absence.* incluyen el tipo de ausencia (data.type) solo si el endpoint se creó con una key que tenga org:ausencias:read, porque el tipo puede revelar una baja médica. Los endpoints creados antes del 28-10-2026 lo siguen recibiendo.
Los webhooks se detienen cuando la cuenta pierde el servicio. Si se desactiva el complemento Public API, los endpoints pasan a disabled y no se reactivan solos al volver a activarlo. Mientras la suscripción no esté activa no se envía ningún evento.
Payload del evento
{ "id": "evt_…", "type": "absence.approved", "created_at": "…", "company_id": "…", "data": { … } }data es deliberadamente pequeño en la mayoría de eventos. Cuando necesites el recurso completo, pídelo por GET con el id que recibes.
Verificación de la firma
Cada entrega llega con tres headers:
| Header | Contenido |
|---|---|
webhook-id | Id único del evento (idempotencia en tu receptor). |
webhook-timestamp | Timestamp Unix del envío. |
webhook-signature | v1,<firma> (puede haber varias separadas por espacio). |
Algoritmo: firma = base64( HMAC_SHA256( key, "{webhook-id}.{webhook-timestamp}.{body_crudo}" ) ), prefijada con v1,.
La key es la parte tras whsec_ del secreto (mostrado una sola vez al crear el webhook), decodificada de base64 a bytes. Firma sobre el cuerpo crudo (bytes exactos recibidos), no sobre el JSON re-serializado.
Verificación (rechaza si no coincide o si webhook-timestamp difiere > 5 min de tu reloj):
Node.js
const crypto = require('crypto');
function verify(secret, headers, rawBody) {
const id = headers['webhook-id'];
const ts = headers['webhook-timestamp'];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // ±5 min
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
const expected = 'v1,' + crypto
.createHmac('sha256', key)
.update(`${id}.${ts}.${rawBody}`)
.digest('base64');
return headers['webhook-signature']
.split(' ')
.some(sig => crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)));
}Verifica y firma sobre el cuerpo crudo (bytes exactos recibidos), no sobre el JSON re-serializado — cualquier reformateo invalida la firma. Usa el webhook-id como clave de idempotencia en tu receptor para descartar reenvíos.
Reintentos
Ante fallo (o respuesta ≠ 2xx) Kinmu reintenta con backoff: 1 m, 5 m, 30 m, 2 h, 12 h, 24 h. Responde 2xx rápido; procesa async si tardas. Un endpoint que falla 7 días seguidos se auto-desactiva.
Puedes reencolar manualmente una entrega fallida con POST /v1/webhook-endpoints/{id}/deliveries/{deliveryId}/retry (usa el id de la entrega que ves en GET /{id}/deliveries).
Solo se pueden reintentar entregas con status=failed; cualquier otro estado devuelve 409 conflict. test y retry admiten 30 peticiones por minuto entre los dos; por encima, responden 429 con Retry-After.
Buenas prácticas
Responde rápido, procesa en background
Verifica la firma, encola el trabajo y responde 2xx de inmediato.
Verifica siempre la firma
No proceses ningún payload sin verificar webhook-signature. Trata el secreto como una credencial.
Sé idempotente
Deduplica por webhook-id. Los reintentos son normales.
Prueba con ping
Usa POST /v1/webhook-endpoints/{id}/test para recibir un evento ping y validar tu integración de punta a punta.