8. Integraciones — Webhooks salientes

Los webhooks salientes permiten a NoviaMind notificar a tus propios sistemas en el momento en que cambian tus datos de CRM — sin necesidad de consultar la API continuamente. Usos típicos: disparar un flujo de n8n / Make / Zapier cuando se captura un nuevo prospecto, o mantener sincronizada una herramienta interna.

Accede desde Integraciones → Webhooks salientes en la sección CRM.


8.9 Eventos disponibles

EventoSe envía cuando
lead.createdSe crea un prospecto
lead.updatedSe actualiza un prospecto
property.createdSe crea una propiedad
property.updatedSe actualiza una propiedad
realtor.createdSe crea un agente
realtor.updatedSe actualiza un agente

Un webhook se suscribe a uno o más eventos — solo esos disparan una entrega a tu URL.


8.10 Crear un webhook

  1. Ve a Integraciones → Webhooks salientes
  2. Haz clic en «Añadir webhook»
  3. Introduce tu URL de retorno (debe ser https:// en producción; http:// solo se acepta para pruebas locales, p. ej. un túnel de ngrok)
  4. Añade opcionalmente una descripción (p. ej. «flujo de n8n en producción»)
  5. Selecciona los eventos a los que suscribirte
  6. Haz clic en «Guardar»

⚠️ Guarda el secreto de firma inmediatamente. Tras la creación, NoviaMind muestra el secreto (whsec_...) una única vez. Cualquier consulta posterior lo muestra enmascarado. Si lo pierdes, elimina el webhook y crea uno nuevo.

💡 Las URL que apuntan a direcciones de red privadas o internas se rechazan por motivos de seguridad.


8.11 Qué recibe tu endpoint

Cada entrega es una petición HTTP POST con un cuerpo JSON:

{
  "id": "d3f5c9a2-...",
  "event": "lead.created",
  "team_id": "b81e07c4-...",
  "created_at": "2026-07-30T14:20:11.000Z",
  "data": { "...": "la entidad creada o actualizada — la forma depende de `event`, ver más abajo" }
}

Y estas cabeceras:

CabeceraContenido
X-Webhook-IdIdentificador único de la entrega (también el id del cuerpo)
X-Webhook-EventNombre del evento (p. ej. lead.created)
X-Webhook-Signaturesha256=<hex> — firma HMAC-SHA256 del cuerpo sin procesar

Formato de data por entidad

data no es un DTO de respuesta depurado: es la fila completa del prospecto/inmueble/agente tal y como está almacenada, por lo que puede incluir más campos de los documentados a continuación (trata los campos desconocidos como compatibles hacia delante, no los rechaces) y cualquier campo nullable que no uses vale simplemente null.

lead.created / lead.updateddata es el prospecto. Captura real (de una llamada con el agente de IA, con los secretos/datos personales acortados y notes y los bloques de recopilación de datos de la IA en custom_fields / raw_data truncados por longitud — en la práctica notes contiene el resumen completo de la llamada y custom_fields.* una entrada por cada campo de recopilación configurado):

{
  "id": "d80029a4-8324-4fad-afe2-ad1af76319fe",
  "team_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
  "source_provider": "noviamind",
  "external_reference": "conv-XXX",
  "first_name": "Didier",
  "last_name": "Deschamps",
  "email": null,
  "phone": "+33618276741",
  "mobile_phone": null,
  "company": null,
  "job_title": null,
  "status": "new",
  "source_channel": "ai_agent",
  "source_channel_detail": null,
  "notes": "L'utilisateur, Didier Deschamps, a contacté l'agent Giulia pour l'achat d'un T3 à Saint-Jean-Pied-de-Port et a demandé à être rappelé par son conseiller habituel. [...truncado — resumen completo de la llamada, puede incluir llamadas anteriores añadidas al final]",
  "tags": ["rent", "ai_agent_lead"],
  "assigned_realtor_external_id": null,
  "interested_property_ids": [],
  "priority": "medium",
  "score": 0,
  "address": null,
  "city": null,
  "postal_code": null,
  "country": null,
  "budget_min": null,
  "budget_max": 8650234,
  "preferred_contact_method": null,
  "next_follow_up_at": null,
  "last_interaction_at": "2026-07-31T14:55:48.316+00:00",
  "sms_consent": false,
  "target_zip_code": "64220",
  "transaction_type": "sale",
  "target_rooms": 3,
  "surface_min_sqm": 20,
  "preferred_area_text": "Saint-Jean-Pied-de-Port",
  "rental_income_3x": null,
  "rental_guarantee_type": "unknown",
  "properties_presented_count": 0,
  "conversion_outcome": "callback_scheduled",
  "visit_datetime_local": null,
  "call_language": "Français",
  "customer_sentiment": "positive",
  "requested_callback": true,
  "disqualifier_reason": "other",
  "custom_fields": {
    "agent_name": "Agence du Pays Basque - Giulia",
    "transaction_type": {
      "value": "sale",
      "rationale": "L'utilisateur a explicitement déclaré vouloir acheter, ce qui correspond à une transaction de type « sale ».",
      "data_collection_id": "transaction_type"
    },
    "requested_callback": {
      "value": true,
      "rationale": "L'utilisateur a explicitement demandé à être rappelé.",
      "data_collection_id": "requested_callback"
    }
  },
  "raw_data": {
    "data_collection_results": {
      "lead_full_name": {
        "value": "Didier Deschamps",
        "rationale": "L'utilisateur a clairement indiqué son nom complet.",
        "data_collection_id": "lead_full_name"
      }
    }
  },
  "provider_created_at": null,
  "provider_updated_at": null,
  "crm_pushed_at": "2026-07-31T12:30:10.829+00:00",
  "created_at": "2026-06-26T16:42:06.236+00:00",
  "updated_at": "2026-07-31T14:55:48.352+00:00"
}

💡 Los identificadores internos de seguimiento de llamadas se eliminan antes de que la carga útil salga de NoviaMind: nunca verás un campo conversation_ids, aunque sea una columna real del registro subyacente.

💡 custom_fields y raw_data.data_collection_results provienen ambos de la extracción de campos de la IA y se solapan en gran medida (raw_data es la extracción tal y como se capturó originalmente; custom_fields es lo que realmente se persiste en el prospecto). Cada entrada lleva value, rationale (por qué la IA extrajo ese valor) y data_collection_id; algunas entradas incluyen además un bloque json_schema que describe el tipo/enum esperado del campo. value es null con frecuencia cuando la IA no encontró nada que extraer para ese campo durante la llamada.

property.created / property.updateddata es el inmueble:

{
  "id": "9c8b7a6d-...",
  "team_id": "b81e07c4-...",
  "is_sale": true,
  "is_rent": false,
  "property_type": "apartment",
  "surface_sqm": 65,
  "price_eur": 320000,
  "rooms": 3,
  "address": "12 Rue de Rivoli",
  "city": "Paris",
  "zip_code": "75004",
  "title": "Bright 3-room apartment near the Seine",
  "tags": ["renovated"],
  "created_at": "2026-07-30T14:20:11.000Z",
  "updated_at": "2026-07-30T14:20:11.000Z"
}

realtor.created / realtor.updateddata es el agente:

{
  "id": "1e2d3c4b-...",
  "team_id": "b81e07c4-...",
  "first_name": "Marc",
  "last_name": "Dupont",
  "email": "[email protected]",
  "phone": "+33698765432",
  "role": "agent",
  "status": "active",
  "license_number": "CPI-1234",
  "service_areas": ["Paris 11e", "Paris 12e"],
  "created_at": "2026-07-30T14:20:11.000Z",
  "updated_at": "2026-07-30T14:20:11.000Z"
}

💡 Las entregas webhook.test (desde el botón «Enviar prueba») no coinciden con ninguna de las formas anteriores: data es simplemente { "message": "This is a test webhook delivery from NoviaMind." }.

Verificar la firma

Calcula un HMAC-SHA256 de los bytes sin procesar del cuerpo de la petición (no del objeto re-serializado/parseado: cualquier diferencia en el orden de las claves o en los espacios hará que la firma no coincida) con tu secreto de firma y compáralo con la cabecera X-Webhook-Signature usando una comparación de tiempo constante. Ejemplo con Node/Express, asumiendo que la ruta tiene acceso al cuerpo sin procesar (p. ej. express.raw({ type: 'application/json' }) montado solo en esta ruta, antes de cualquier parser de cuerpo JSON):

const crypto = require('crypto');

function isValidSignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader?.startsWith('sha256=')) return false;

  const expectedHex = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const expected = Buffer.from(expectedHex, 'utf8');
  const received = Buffer.from(signatureHeader.slice('sha256='.length), 'utf8');

  // timingSafeEqual lanza una excepción si los buffers tienen longitudes distintas: compara primero las longitudes.
  if (expected.length !== received.length) return false;

  return crypto.timingSafeEqual(expected, received);
}

app.post('/webhooks/noviamind', express.raw({ type: 'application/json' }), (req, res) => {
  const valid = isValidSignature(
    req.body,
    req.headers['x-webhook-signature'],
    process.env.WEBHOOK_SECRET,
  );
  if (!valid) return res.status(401).send('invalid signature');

  const event = JSON.parse(req.body); // aquí req.body es un Buffer: parséalo solo tras verificar
  // ... maneja event.event / event.data
  res.sendStatus(200);
});

💡 En n8n, el nodo Webhook te da el cuerpo sin procesar mediante su opción de datos binarios/raw: verifícalo en un nodo Code con la misma lógica antes de confiar en event.data, o simplemente mantén la URL en secreto si la verificación de firma no es necesaria para tu caso de uso.

Tu endpoint debe responder con un estado 2xx en menos de 10 segundos. Cualquier otra respuesta cuenta como fallo.


8.12 Probar un webhook

Haz clic en «Enviar prueba» en la fila de un webhook. NoviaMind envía un evento sintético webhook.test, firmado exactamente igual que una entrega real, y te muestra el estado HTTP que devolvió tu endpoint — ideal para validar tu flujo de n8n o tu verificación de firma antes de pasar a producción.


8.13 Reintentos, fallos y desactivación automática

  • Cada evento se entrega con hasta 5 intentos y backoff exponencial.
  • La fila del webhook muestra el estado de la última entrega y el número de fallos consecutivos.
  • Tras 20 fallos consecutivos, el webhook se desactiva automáticamente para que una URL muerta deje de consumir intentos de entrega. Arregla tu endpoint y haz clic en «Reactivar».
  • Cualquier entrega exitosa pone el contador de fallos a cero.

Historial de entregas

Haz clic en «Historial» para ver las últimas entregas (hasta 50, conservadas 30 días): fecha y hora, evento, estado HTTP y número de intento. Solo se almacenan metadatos del resultado — nunca el contenido del payload.


8.14 Gestionar webhooks

AcciónQuién puede hacerlo
Ver webhooks e historialTodos los miembros del equipo
Crear, editar, probar, activar/desactivarAdministradores y miembros regulares
EliminarSolo administradores
  • Activar / Desactivar: pausa las entregas sin perder la configuración (interruptor en cada fila).
  • Editar: cambia la URL, la descripción o los eventos suscritos en cualquier momento. El secreto de firma nunca cambia.
  • Rotar el secreto: elimina el webhook y crea uno nuevo.

💡 Los desarrolladores encontrarán la referencia completa de la API (endpoints, esquemas, detalles de firma) en la documentación de la API enlazada desde el diálogo de webhooks.