Webhooks: notificar eventos a tus propios sistemas
Al terminar este artículo tendrás un webhook configurado que llama a tu propia dirección con los eventos que hayas elegido, con clave de firma, entrega de prueba, registro y reintentos automáticos en caso de error.
Un webhook invierte el sentido: ya no es tu sistema el que pregunta a Univents, sino Univents el que avisa a tu sistema en cuanto ocurre algo, por ejemplo cuando entra una solicitud, se acepta un presupuesto o se emite una factura. Así, tus propios programas, scripts o servicios de automatización se conectan a tu workspace sin que nadie tenga que consultar de forma periódica.
Se configura en el propio workspace, en Integraciones → Desarrolladores → Webhooks. La configuración, las claves y el registro solo son visibles para quien tiene el rol Propietario, porque ahí figura a qué direcciones llama Univents.
Cuándo merece la pena un webhook
Usa un webhook cuando tu propio sistema deba reaccionar de inmediato: reflejar una solicitud en tu CRM, disparar el envío a un sistema de almacén, trasladar una factura a tu contabilidad, actualizar un panel informativo. Para todo lo que deba quedarse dentro de Univents, el lugar adecuado son las Automatizaciones.
Crear un endpoint
- Haz clic en Integraciones en el menú principal de la izquierda, después en la categoría Desarrolladores y abre la tarjeta Webhooks.
- Haz clic en Crear webhook, arriba a la derecha.
- En Nombre, escribe algo que luego reconozcas, por ejemplo
CRM-Sync. - En Dirección, introduce la dirección
https://completa de tu receptor. Solohttps: las direcciones sin cifrar se rechazan. Univents también rechaza las direcciones de tu red local, delocalhosto de una red interna. - Marca los Eventos que debe recibir este receptor.
- Guarda. Univents te mostrará ahora, una sola vez, la clave de firma.
La clave de firma
La clave empieza por whsec_. Se muestra exactamente una vez: al crearla y al regenerarla. Después solo verás el principio, para que en el workspace se pueda saber qué clave está guardada sin que el valor completo quede a la vista. Cópiala por tanto de inmediato en tu sistema (variable de entorno, almacén de secretos).
Con Regenerar clave de firma obtienes una nueva en cualquier momento. La anterior deja de ser válida al instante: cámbiala en tu lado en ese mismo momento; de lo contrario, tu receptor rechazará los mensajes entrantes.
Verificar la firma
Univents envía tres cabeceras con cada entrega:
| Cabecera | Contenido |
|---|---|
X-Univents-Signature | t=<Zeitstempel>,v1=<Signatur> |
X-Univents-Event | la clave del desencadenante, por ejemplo inquiry_created |
X-Univents-Delivery-Id | el identificador de esta entrega |
La firma es un HMAC-SHA256 sobre el texto ${t}.${Body}: es decir, la marca de tiempo, un punto y el body en bruto, byte a byte tal como llega. Importante: verifica el body en bruto, no uno serializado de nuevo. Un JSON.parse seguido de JSON.stringify cambia el orden de los campos y los escapes, y la verificación falla aunque el mensaje sea correcto.
Así se ve la verificación en Node:
const crypto = require('node:crypto');
const express = require('express');
const app = express();
// Roher Body — NICHT express.json() davor.
app.post('/webhooks/dein-system', express.raw({ type: '*/*' }), (req, res) => {
const header = req.get('X-Univents-Signature') ?? '';
const parts = Object.fromEntries(
header.split(',').map((piece) => piece.trim().split('=')),
);
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(`${parts.t}.${req.body.toString('utf8')}`)
.digest('hex');
const received = parts.v1 ?? '';
// Länge zuerst prüfen: timingSafeEqual wirft bei ungleicher Länge.
const ok =
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!ok) return res.status(400).send('signature mismatch');
// Der Zeitstempel liegt unter der Signatur — so kann niemand eine alte
// Meldung erneut einspielen.
const ageSeconds = Math.abs(Date.now() / 1000 - Number(parts.t));
if (ageSeconds > 300) return res.status(400).send('stale');
const event = JSON.parse(req.body.toString('utf8'));
console.log(req.get('X-Univents-Event'), event);
res.sendStatus(200);
});
Responde con 200 (o con cualquier otra respuesta del rango 2xx) en cuanto hayas aceptado el mensaje. Cualquier otra respuesta, o una respuesta que tarde más de diez segundos, se considera un intento fallido.
Entregas: qué ocurre cuando hay errores
La entrega se realiza al menos una vez, no exactamente una. Un corte de conexión por el camino puede hacer que la misma operación llegue a tu sistema una segunda vez. Por eso, diseña tu receptor de modo que una llamada repetida no estropee nada: comprueba el X-Univents-Delivery-Id con los últimos identificadores procesados antes de crear nada.
Si tu receptor no responde o responde con un error, Univents lo vuelve a intentar. En total están previstos ocho intentos, con intervalos crecientes:
| Intento | Intervalo desde el anterior |
|---|---|
| 1 | inmediato |
| 2 | 1 minuto |
| 3 | 5 minutos |
| 4 | 30 minutos |
| 5 | 2 horas |
| 6 | 6 horas |
| 7 | 12 horas |
| 8 | 24 horas |
Después, el mensaje se da por fallido definitivamente y no se vuelve a intentar. Si tu sistema está en mantenimiento programado, así dispones de casi dos días para volver a estar accesible.
Entrega de prueba
Con Enviar prueba envías un mensaje a un endpoint sin esperar a un evento real. Así compruebas, tras la configuración, que la dirección, la verificación de la firma y la vía de respuesta funcionan. La entrega de prueba aparece después en el registro, para que no la confundas con un mensaje real.
Un endpoint en estado En pausa no recibe entregas de prueba: actívalo primero. Y como la entrega de prueba es la única vía por la que puedes hacer que nuestro servidor llame a una dirección que tú decides, existe un límite de diez entregas de prueba por workspace y hora. Superado el límite, la interfaz te indica que lo intentes de nuevo más tarde.
Registro y reenvío
Bajo cada endpoint figuran las últimas 50 entregas. En cada fila ves:
- el estado: en espera, en curso, entregada o fallida definitivamente,
- el evento,
- el número de intentos (por ejemplo
3/8), - el estado de la respuesta de tu receptor.
Con Ver contenido ves el mensaje enviado, y con Detalles, el último error y el momento del próximo intento. Univents conserva la respuesta de tu receptor, abreviada, como material de diagnóstico.
Reenviar vuelve a enviar un mensaje entregado o fallido definitivamente, algo útil cuando tu sistema solo ha estado fuera de servicio un momento. Mientras una entrega está en curso o aún en espera, el botón permanece bloqueado, para que no se produzcan dos llamadas a la vez.
Pausar y eliminar
El interruptor del endpoint lo pausa: Univents deja de entregarle mensajes de inmediato, pero la configuración se conserva, lo ideal para un sistema en reformas. Univents lee el estado de nuevo en cada entrega, así que la pausa no surte efecto solo con el siguiente evento.
Eliminar borra el endpoint junto con su registro. Univents te pide confirmación antes.
Eventos disponibles
Puedes elegir entre los mismos eventos que conocen las automatizaciones. La lista del diálogo crece con el tiempo: en cuanto Univents notifica un evento en un punto nuevo, este también está disponible para los webhooks. Un evento sin marcar no se entrega.
Qué contiene el mensaje
Todos los mensajes tienen el mismo envoltorio:
{
"id": "48213",
"type": "inquiry_created",
"created_at": "2026-09-28T10:12:33.000Z",
"data": { "…": "…" }
}
id: el identificador de esta entrega, idéntico a la cabeceraX-Univents-Delivery-Id. Úsalo como clave de idempotencia.type: la clave del desencadenante (inquiry_created,event_confirmed, …).created_at: cuándo se construyó el envoltorio, no cuándo ocurrió el evento.data: el contenido funcional, según una lista fija para cada evento.
En data figura únicamente lo que consta en esa lista. Los campos internos y todo lo que pudiera contener credenciales o texto libre se omiten, entre otros legacy_*, stripe_* (identificadores de pago), *token*, *secret*, *_hash*, pdf_url, campos de correo electrónico y teléfono, notas/descripciones y los interruptores internos (is_*, created_by, deleted_at). Un campo sin valor se omite, no se envía como null. Las columnas nuevas de nuestras tablas no aparecen aquí por sí solas; las incorporamos de forma deliberada, una por una.
Cada versión de data incluye entity_type y entity_id, con lo que puedes asignar el mensaje incluso cuando el evento no tiene un registro propio.
Solicitudes
| Evento | Campos en data |
|---|---|
inquiry_created | id, event_id, contact_id, location_id, booking_page_id, booking_request_state, event_name, event_date, event_end_date, guest_count, estimated_value, account_id, created_at, updated_at |
quote_accepted | los campos de inquiry_created y los de un presupuesto (consulta Documentos financieros) |
Eventos
| Evento | Campos en data |
|---|---|
event_confirmed, event_cancelled | id, name, guest_count, budget, start_date, end_date, date_is_confirmed, language, event_state, location_id, room_id, contact_id, account_id, created_at, updated_at |
event_t_minus | como arriba, más days_before, weekday |
event_t_plus | como arriba, más days_after |
Documentos financieros
Se aplica a invoice_overdue, invoice_paid, quote_sent, quote_expired y quote_rejected; en quote_rejected, además, source.
id, event_id, contact_id, doc_type, document_number, title, version, quote_state, invoice_state, issue_date, due_date, sent_date, accepted_date, rejected_date, expires_at, net_total, gross_total, tax_total, discount_total, deposit_amount, currency, linked_order_id, prior_version_id, account_id, created_at, updated_at
Otros eventos
| Evento | Campos en data |
|---|---|
payment_received | id, booking_request_id, event_id, contact_id, booking_order_state, total_net, total_gross, total_tax, discount_total, currency, payment_method, paid_at, account_id, created_at, updated_at |
contract_signed | id, event_id, contact_id, title, contract_doc_type, contract_state, version, sent_at, expires_at, has_signed_pdf, account_id, created_at, updated_at |
email_received | contact_id, received_at, has_attachments, has_contact_match; sin contenido del correo, sin asunto |
schedule_monthly | day_of_month, target_date, period_start, period_end, period_label, account_id |
La entrega de prueba usa el mismo envoltorio con type: "webhook_test" y data: { test, endpoint_id, sent_at }.
Un evento para el que aquí no figura ninguna lista llega con un data vacío: mejor un envoltorio vacío y visible que un registro en bruto.
Límites
- Solo https, sin redirecciones: si tu servidor responde con una redirección, se considera un intento fallido. Introduce la dirección definitiva.
- Diez segundos por intento. Todo trabajo que exceda ese tiempo debe ir detrás de la respuesta: acepta el mensaje, responde con 200 y procésalo después.
- El contenido del mensaje describe el registro que desencadenó el evento: trátalo como datos de clientes de tu workspace y no lo registres en otros sitios sin comprobarlo.