Webhooks salientes
Con un webhook saliente, exoCortex envía una solicitud POST a tu sistema cada vez que ocurre un evento al que te suscribiste: un ticket creado, un cambio de estado, un SLA por vencer. Así tu sistema reacciona de inmediato, sin consultar la API a intervalos.
Crear una suscripción
Sección titulada «Crear una suscripción»- En exoCortex Core abre Integrations → Webhooks. Necesitas un rol administrativo.
- Indica la URL que recibirá los eventos. Debe ser
https://. - Elige los eventos.
*significa todos los eventos. - Elige el formato:
json: el formato estándar de exoCortex, que se describe en esta página;datadog,pagerduty,slack-block: el formato nativo de esos servicios, para usarlo cuando la URL es la suya.
- Si tu gateway lo requiere, agrega encabezados fijos, por ejemplo un encabezado de autenticación.
- Guarda. Si no indicaste un secreto de firma, exoCortex genera uno (
whsec_…) y lo muestra una sola vez: guárdalo, lo necesitas para verificar las firmas.
Con la acción de prueba envías a la suscripción el evento webhook.test y compruebas que tu sistema lo reciba.
Eventos
Sección titulada «Eventos»| Evento | Cuándo llega |
|---|---|
ticket.created |
Se creó un ticket |
ticket.comment_added |
Se agregó un comentario |
ticket.field_changed |
Cambió un campo |
ticket.assignee_changed |
Cambió el responsable asignado |
ticket.status_changed |
Cambió el estado |
ticket.priority_changed |
Cambió la prioridad |
ticket.sla_warning |
Un SLA está por vencer |
ticket.sla_breach |
Un SLA venció |
ticket.resolved |
El ticket se resolvió |
ticket.closed |
El ticket se cerró |
ticket.reopened |
El ticket se reabrió |
ticket.link_created |
Se creó un vínculo entre tickets |
ticket.link_deleted |
Se eliminó un vínculo entre tickets |
routing.no_match |
Ninguna regla de enrutamiento encontró una cola para el ticket |
routing.manual_routed |
Un operador enrutó el ticket a mano |
webhook.test |
Enviaste un evento de prueba a la suscripción |
Además de estos, puedes suscribirte por nombre a los eventos de las reglas de notificación de tu tenant, por ejemplo approval_requested.
exoCortex puede agregar nuevos tipos de evento: tu sistema debe ignorar los que no conozca.
Formato del evento
Sección titulada «Formato del evento»Todos los eventos en formato json tienen la misma estructura:
{ "id": "evt_8a1f4c2e9b7d4e3f8a6b5c4d3e2f1a00", "type": "ticket.status_changed", "created_at": "2026-11-01T09:31:47.120Z", "api_version": "1", "tenant_id": "ACME", "data": { "ticket": { "key": "ACME-1042", "summary": "Payment gateway returns HTTP 502", "status": "In Progress", "priority": "High", "issuetype": "IT-INC", "assignee": "jane.doe", "created": "2026-11-01T09:14:05.000Z", "updated": "2026-11-01T09:31:47.000Z" }, "actor": "jane.doe", "changes": { "from": "Open", "to": "In Progress" } }}ididentifica el evento y es el mismo para todas las suscripciones que lo reciben.actores el usuario que causó el evento, osystem.changescontiene los detalles del evento: por ejemplo, el estado anterior y el nuevo, o el texto del comentario.
Hasta el 30 de junio de 2027, el cuerpo también contiene los campos obsoletos event, timestamp y source y una copia de los campos de data en el primer nivel. Un sistema nuevo debe leer solo los campos que se describen arriba.
Encabezados
Sección titulada «Encabezados»| Encabezado | Ejemplo | Significado |
|---|---|---|
X-Exo-Timestamp |
1793491200 |
Momento de la firma, en segundos Unix |
X-Exo-Signature |
v1=5257a869… |
Firma del evento |
X-Exo-Event |
ticket.created |
Tipo de evento, igual a type |
X-Exo-Delivery |
dlv_0b9d… |
Identificador único del intento de entrega |
Verificar la firma
Sección titulada «Verificar la firma»Antes de confiar en un evento, verifica la firma:
- Lee el cuerpo tal como llegó, sin interpretar ni reescribir el JSON: los bytes cambiarían.
- Descarta la solicitud si
X-Exo-Timestampfalta o difiere más de 300 segundos de tu reloj. Así bloqueas las solicitudes repetidas. Mantén el reloj sincronizado con NTP. - Calcula el HMAC-SHA256 de
timestamp + "." + bodycon el secreto de la suscripción, en hexadecimal en minúsculas. - Compara el resultado, en tiempo constante, con cada valor
v1=deX-Exo-Signature. Durante la rotación del secreto, el encabezado puede contener más de uno, separados por comas.
Ejemplo en Node.js:
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyExoWebhook(rawBody, headers, secret, toleranceSec = 300) { const ts = headers['x-exo-timestamp']; const sigHeader = headers['x-exo-signature'] || ''; if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > toleranceSec) return false; const expected = Buffer.from(createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex')); return sigHeader .split(',') .map((s) => s.trim()) .filter((s) => s.startsWith('v1=')) .some((s) => { const got = Buffer.from(s.slice(3)); return got.length === expected.length && timingSafeEqual(got, expected); });}Ejemplo en Python:
import hashlib, hmac, time
def verify_exo_webhook(raw_body: bytes, headers, secret: str, tolerance_sec: int = 300) -> bool: ts = headers.get('X-Exo-Timestamp') if not ts or abs(time.time() - int(ts)) > tolerance_sec: return False expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest() sigs = [s.strip()[3:] for s in headers.get('X-Exo-Signature', '').split(',') if s.strip().startswith('v1=')] return any(hmac.compare_digest(s, expected) for s in sigs)El encabezado X-NH-Signature, que firma solo el cuerpo sin timestamp, es obsoleto y se eliminará el 30 de junio de 2027.
Responder y manejar los duplicados
Sección titulada «Responder y manejar los duplicados»- Responde con cualquier código
2xxen un máximo de 5 segundos. Si tienes que hacer un trabajo largo, ponlo en cola y responde de inmediato. - La entrega está garantizada al menos una vez: el mismo evento puede llegar más de una vez. Usa
idoX-Exo-Deliverypara reconocer los duplicados, por ejemplo guardando los identificadores que viste en los últimos 5 minutos. - Los eventos pueden llegar en un orden distinto al que ocurrieron: ordénalos con
created_at.