# Webhooks salientes Notificación de eventos de la clínica hacia sistemas externos, con patrón **outbox + cron + reintentos + bitácora**. Sin webhooks activos suscritos no se crea ninguna fila ni se envía nada (cero impacto). ## Modelos ### `skeen.webhook` | Campo | Descripción | | --- | --- | | `name`, `url` | Nombre y URL destino | | `eventos` | Claves separadas por coma; vacío = todos | | `activo` | Habilitado | | `secreto` | Con este secreto se firma cada entrega | | `headers` | Cabeceras extra (JSON, opcional) | | `notas`, `creado_por_login` | Metadatos | | `ultima_entrega_at` | Última entrega correcta | | `fallos_consecutivos` | Se desactiva tras N fallos (default 20, configurable en `skeen.webhooks.fallos_max`) | ### `skeen.webhook.evento` (outbox) `webhook_id`, `evento`, `payload` (JSON), `estado` (`pendiente|enviado|fallido|descartado`), `intentos`, `proximo_intento`, `respuesta_codigo`, `respuesta_texto`, `enviado_at`, `clave_unica` (idempotencia por evento+objeto). ## Catálogo de eventos | Evento | Cuándo se emite | | --- | --- | | `cita.creada` | Al crear una cita | | `cita.confirmada` | `state` → `confirmed` | | `cita.cancelada` | `state` → `cancelled` | | `cita.no_show` | `state` → `no_show` | | `cita.completada` | `state` → `done` | | `venta.pagada` | `state` → `paid` | | `venta.cancelada` | `state` → `cancelled` o `refunded` | | `pago.registrado` | Al crear un `skeen.pago` | | `paciente.creado` | Al crear un `res.partner` con `is_patient` | | `membresia.renovada` | `action_renovar` | | `formulario.llenado` | `estado` → `llenado` | | `compra.recibida` | `estado` → `recibida`/`recibida_parcial` | ### Ejemplo de payload (`cita.creada`) ```json { "id": 123, "reference": "CITA-0123", "state": "pending", "date": "2026-10-20", "patient_id": 55, "patient": "María López", "service_id": 7, "servicio": "Limpieza facial", "doctor": "Dra. Pérez" } ``` ## Entrega Cron cada 5 minutos (`skeen.webhook.service._cron_tick`) que toma los pendientes vencidos y hace `POST` JSON con: | Cabecera | Valor | | --- | --- | | `X-Skeen-Event` | Clave del evento | | `X-Skeen-Delivery` | UUID de la entrega | | `X-Skeen-Signature` | `sha256=` | * Timeouts cortos (5 s conexión / 15 s lectura). * **Reintentos con backoff**: 1, 5, 30 min, 2 h, 6 h (tope 5 intentos); al agotarlos el evento queda `fallido`. * Tras N fallos consecutivos (default 20) el webhook se desactiva. ### Verificar la firma (Python) ```python import hashlib, hmac firma = request.headers['X-Skeen-Signature'] # 'sha256=...' esperada = 'sha256=' + hmac.new( SECRETO.encode(), request.body, hashlib.sha256).hexdigest() assert hmac.compare_digest(firma, esperada) ``` ### Verificar la firma (Node.js) ```js const crypto = require('crypto'); const esperada = 'sha256=' + crypto .createHmac('sha256', SECRETO) .update(req.rawBody) .digest('hex'); ``` ## Endpoints | Método | Ruta | Acceso | | --- | --- | --- | | GET/POST | `/skeen/frontend/v1/webhooks` | permiso `webhooks.editar` (admin/gerencia) | | PUT/DELETE | `/skeen/frontend/v1/webhooks/` | idem | | GET | `/skeen/frontend/v1/webhooks//eventos` | idem (filtra por `estado`, `evento`, `limit`) | | POST | `/skeen/frontend/v1/webhooks//probar` | idem (evento de prueba) | | POST | `/skeen/frontend/v1/webhook_eventos//reintentar` | idem | ## UI Configuración → pestaña **Webhooks** (admin/gerencia): CRUD, selector de eventos, secreto (generado o propio), visor del payload de ejemplo, log de entregas con estado/código/fecha, reintento manual y "probar".