Webhooks

Con los webhooks, tu aplicación se entera en el momento en que algo ocurre en Edugoverna — se registra una decisión de consentimiento, entra una solicitud ARCO por el portal, falla el envío de un mensaje — sin necesidad de consultar la API en un ciclo de polling. En esta guía verás cómo se firman las entregas, qué forma tiene el sobre de cada evento y el catálogo completo de eventos disponibles.

Registrar un endpoint

Los endpoints de webhook se administran desde la Consola (Integraciones → Webhooks) o mediante la API de gestión de webhooks. Al crear un endpoint eliges un nombre, la URL de destino (solo https://), los eventos a los que te suscribes y, opcionalmente, un secreto propio. Si omites el secreto, el servidor genera uno con prefijo whsec_ y lo devuelve una única vez en la respuesta de creación.

Los webhooks requieren que la organización tenga habilitado el módulo integrations en su licencia. Sin ese módulo, los eventos pendientes se omiten en silencio, igual que en una organización sin endpoints activos.


Cabeceras de cada entrega

Cada entrega es un POST con content-type: application/json a la URL del endpoint, acompañado de tres cabeceras:

  • Nombre
    x-edugoverna-signature
    Tipo
    string
    Descripción

    Firma HMAC-SHA-256 en hexadecimal, calculada como HMAC(secret, "{timestamp}.{rawBody}"). El nombre de esta cabecera es configurable por endpoint (signatureHeader); el valor por defecto es x-edugoverna-signature.

  • Nombre
    x-edugoverna-event
    Tipo
    string
    Descripción

    El tipo de evento entregado, por ejemplo consent.decision.recorded.

  • Nombre
    x-edugoverna-timestamp
    Tipo
    string
    Descripción

    El instante en que se firmó la solicitud, en segundos Unix. Úsalo para reconstruir la cadena firmada y para rechazar entregas antiguas.


Verificación de la firma

Para comprobar que una entrega proviene efectivamente de Edugoverna:

  1. Lee la cabecera x-edugoverna-timestamp y el cuerpo crudo de la solicitud (los bytes exactos recibidos — no re-serialices el JSON ya parseado).
  2. Construye la cadena firmada como ${timestamp}.${rawBody}, con un punto literal entre el timestamp y el cuerpo.
  3. Calcula HMAC-SHA-256(secret, cadenaFirmada) en hexadecimal y compáralo con la cabecera de firma usando una comparación de tiempo constante.
  4. Rechaza la entrega si |ahoraEnSegundos - timestamp| > 300 (5 minutos), para protegerte de la repetición de una solicitud capturada.

Verificar una entrega

import crypto from "node:crypto"

const TOLERANCE_SECONDS = 300 // 5 minutos

function verifyWebhook({ rawBody, headers, secret }) {
  const timestamp = headers["x-edugoverna-timestamp"]
  const signature = headers["x-edugoverna-signature"]

  if (!timestamp || !signature) {
    return false
  }

  // Rechaza entregas demasiado antiguas (protección anti-replay).
  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp))
  if (!Number.isFinite(age) || age > TOLERANCE_SECONDS) {
    return false
  }

  // Recalcula la firma sobre `${timestamp}.${rawBody}`.
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex")

  // Comparación de tiempo constante.
  const a = Buffer.from(expected, "hex")
  const b = Buffer.from(signature, "hex")
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

Guarda el secreto del webhook con el mismo cuidado que una credencial de API: quien lo conozca puede fabricar entregas válidas. Si sospechas que se filtró, rota el secreto — la rotación genera uno nuevo y lo devuelve una sola vez.


El sobre del evento

Toda entrega comparte el mismo sobre. Los campos específicos del evento viajan dentro de data.

  • Nombre
    event
    Tipo
    string
    Descripción

    El tipo de evento, uno de los del catálogo. Coincide con la cabecera x-edugoverna-event.

  • Nombre
    sourceType
    Tipo
    string
    Descripción

    La clase de recurso que originó el evento: consent_request, consent_delivery, rights_request, security_incident o incident_notification.

  • Nombre
    sourceId
    Tipo
    string
    Descripción

    El identificador del recurso de origen. Úsalo para consultar el estado actual por la API si lo necesitas.

  • Nombre
    occurredAt
    Tipo
    string
    Descripción

    Fecha y hora ISO 8601 en que se construyó la entrega.

  • Nombre
    data
    Tipo
    object | null
    Descripción

    La proyección del recurso de origen en el momento de la entrega. Su forma depende de sourceType — ver los payloads por sourceType.

Sobre de ejemplo

{
  "event": "consent.decision.recorded",
  "sourceType": "consent_request",
  "sourceId": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
  "occurredAt": "2026-08-25T14:03:22.511Z",
  "data": {
    "id": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
    "status": "granted"
    // ... proyección completa según sourceType
  }
}

Catálogo de eventos

Estos son los 16 tipos de evento del catálogo, agrupados igual que en el selector de la Consola. La suscripción es por lista explícita o mediante el comodín ["*"].

Consentimiento

Eventos sobre el ciclo de vida de una solicitud de consentimiento. sourceType es consent_request. Ver Consentimientos.

  • Nombre
    consent.requested
    Descripción

    Se creó una solicitud de consentimiento (desde la Consola, una campaña o la API) y sus entregas quedaron encoladas.

  • Nombre
    consent.decision.recorded
    Descripción

    Se registró una decisión (otorgada o denegada). En campañas sobre padrones compartidos se emite una copia para cada parte — ver la proyección para partners.

  • Nombre
    consent.revoked
    Descripción

    El titular o un operador revocó un consentimiento otorgado.

  • Nombre
    consent.expired
    Descripción

    Una solicitud pendiente pasó su expiresAt sin decisión y fue marcada como expirada por el barrido programado.

consent.decision.recorded

{
  "event": "consent.decision.recorded",
  "sourceType": "consent_request",
  "sourceId": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
  "occurredAt": "2026-08-25T14:03:22.511Z",
  "data": {
    "id": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
    "status": "granted",
    "decisionActorType": "guardian",
    "requestedAt": "2026-08-20T12:00:00.000Z",
    "expiresAt": "2026-09-20T12:00:00.000Z",
    "deliveries": [
      {
        "channel": "email",
        "status": "opened",
        "destinationMasked": "m…a@example.cl"
      }
    ],
    "latestDecision": {
      "decision": "granted",
      "decidedAt": "2026-08-25T14:03:21.000Z",
      "captureMethod": "portal_link_otp"
    }
    // ... campos adicionales de la proyección
  }
}

Envíos de consentimiento

Eventos sobre los mensajes individuales (email, WhatsApp) con que se solicita el consentimiento.

  • Nombre
    consent.delivery.updated
    Descripción

    Una entrega cambió de estado (queued, sent, delivered, opened, failed, …), ya sea por confirmación del proveedor de mensajería o por una actualización manual. sourceType es consent_delivery y la entrega usa el nuevo estado como occurrenceKey, así que cada transición llega una vez.

  • Nombre
    consent.delivery.opened
    Descripción

    El titular o apoderado abrió por primera vez el enlace del portal de consentimiento. sourceType es consent_request: el payload es la solicitud completa, no la fila de la entrega.

  • Nombre
    consent.delivery.failed
    Descripción

    El proveedor reportó una falla definitiva para esa entrega. sourceType es consent_delivery; se emite además del consent.delivery.updated correspondiente.

consent.delivery.failed

{
  "event": "consent.delivery.failed",
  "sourceType": "consent_delivery",
  "sourceId": "7c1f00aa-2e43-4b7e-9f14-3d2e9a7c51d0",
  "occurredAt": "2026-08-25T09:12:44.008Z",
  "data": {
    "id": "7c1f00aa-2e43-4b7e-9f14-3d2e9a7c51d0",
    "consentRequestId": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
    "channel": "whatsapp",
    "destinationMasked": "+569••••1234",
    "status": "failed",
    "failedAt": "2026-08-25T09:12:40.000Z",
    "failureReason": "Provider delivery failed."
    // ... campos adicionales de la fila de entrega
  }
}

Derechos ARCO

Eventos sobre solicitudes de derechos. sourceType es rights_request y data trae la proyección completa de la solicitud tal como la ve la Consola. Ver Derechos ARCO.

  • Nombre
    rights_request.created
    Descripción

    Entró una solicitud nueva, por ejemplo a través del portal ARCO público.

  • Nombre
    rights_request.updated
    Descripción

    La solicitud recibió actividad nueva, como un mensaje de seguimiento del titular por el portal.

  • Nombre
    rights_request.document.added
    Descripción

    El titular adjuntó uno o más documentos a la solicitud (al crearla o en un seguimiento posterior).

  • Nombre
    rights_request.export_ready
    Descripción

    El paquete de exportación aprobado terminó de construirse y quedó disponible para descarga. Se entrega con sourceType: "rights_request" y un occurrenceKey único por construcción del paquete.

  • Nombre
    rights_request.closed
    Descripción

    La solicitud fue cerrada (respondida o denegada) con su certificado de cierre.

rights_request.created

{
  "event": "rights_request.created",
  "sourceType": "rights_request",
  "sourceId": "9e2a41d7-6c3b-4f0e-8a17-b5d4c2e8f901",
  "occurredAt": "2026-08-25T16:40:02.113Z",
  "data": {
    "id": "9e2a41d7-6c3b-4f0e-8a17-b5d4c2e8f901",
    "requestType": "access",
    "status": "received"
    // ... proyección completa de la solicitud
  }
}

Plazos ARCO

Eventos sobre el plazo legal de respuesta de una solicitud de derechos. Estos tipos forman parte del catálogo suscribible (y del comodín ["*"]), pero la plataforma aún no los emite: hoy el seguimiento de plazos notifica por correo al equipo responsable. Cuando se emitan, sourceType será rights_request.

  • Nombre
    rights_request.sla_due_soon
    Descripción

    Una solicitud abierta entró en la ventana previa a su vencimiento.

  • Nombre
    rights_request.sla_overdue
    Descripción

    Una solicitud abierta superó su fecha de vencimiento sin cierre.

Incidentes

Eventos sobre incidentes de seguridad y sus notificaciones a titulares.

  • Nombre
    security.incident.created
    Descripción

    Se registró un incidente de seguridad y se encolaron las notificaciones a los titulares afectados. sourceType es security_incident.

  • Nombre
    security.incident.notification.updated
    Descripción

    La notificación de incidente a un titular cambió de estado según el proveedor de mensajería. sourceType es incident_notification y el nuevo estado viaja como occurrenceKey.

security.incident.created

{
  "event": "security.incident.created",
  "sourceType": "security_incident",
  "sourceId": "4f8b2c6d-1a0e-4d3f-9c7b-2e5a8d1f6b09",
  "occurredAt": "2026-08-25T18:05:37.902Z",
  "data": {
    "id": "4f8b2c6d-1a0e-4d3f-9c7b-2e5a8d1f6b09"
    // ... fila del incidente
  }
}

Payloads por sourceType

El contenido de data es la proyección del recurso de origen en el momento en que se construye la entrega (no en el momento del hecho: si el recurso cambió entre medio, verás su estado más reciente).

  • Nombre
    consent_request
    Tipo
    object
    Descripción

    La proyección completa de la solicitud de consentimiento: la fila principal (id, status, decisionActorType, processingActivityVersionId, targetSubjectId, requestedAt, expiresAt, …) más deliveries[], latestDecision (con su desglose de opciones y evidencia) y latestEvent. Si la solicitud pertenece a una campaña se agregan campaignId, campaignMode, campaignPublicSubjectFlow, campaignIdentityVerificationMode, campaignStatus, campaignContactEntryId y campaignContactEntry.

  • Nombre
    consent_delivery
    Tipo
    object
    Descripción

    La fila de la entrega: id, consentRequestId, channel, destinationMasked, status, deliveryProvider, providerMessageId, las marcas de tiempo queuedAt / sentAt / deliveredAt / openedAt / failedAt y failureReason.

  • Nombre
    rights_request
    Tipo
    object
    Descripción

    La proyección completa de la solicitud de derechos, incluida su actividad asociada (documentos, mensajes, cierre), tal como la entrega la API de la Consola.

  • Nombre
    security_incident
    Tipo
    object
    Descripción

    La fila del incidente de seguridad.

  • Nombre
    incident_notification
    Tipo
    object
    Descripción

    La fila de la notificación de incidente a un titular, con su estado de entrega.


Proyección para partners (shared_partner)

En una campaña sobre un padrón compartido, la campaña vive en la organización del partner mientras la solicitud de consentimiento vive en la del sostenedor de los datos. Cuando se registra una decisión, el evento consent.decision.recorded se emite para ambas partes con el mismo occurrenceKey:

  • La copia del sostenedor lleva la proyección completa descrita arriba.
  • La copia que reciben los endpoints de la organización del partner lleva una proyección reducida, construida como lista blanca campo a campo. Se identifica a sí misma con "projection": "shared_partner" para que nunca se confunda con una proyección completa a la que "le faltan" campos.

La proyección reducida se limita a lo que el partner ya puede leer por su propio endpoint de sujetos compartidos: no incluye RUT (ni enmascarado), contactos en ninguna forma, el nombre del titular, el id interno del sujeto en la organización dueña, ni evidencia o texto libre de operadores. latestDecision.decisionAuthority indica el rol que decidió (titular, apoderado), nunca quién.

Payload shared_partner

{
  "event": "consent.decision.recorded",
  "sourceType": "consent_request",
  "sourceId": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
  "occurredAt": "2026-08-25T14:03:22.511Z",
  "data": {
    "projection": "shared_partner",
    "consentRequestId": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
    "dataOwnerOrganizationId": "d290f1ee-6c54-4b01-90e6-d701748f0851",
    "registryShareId": "a3bb189e-8bf9-3888-9912-ace4e6543002",
    "campaignId": "16fd2706-8baf-433b-82eb-8c7fada847da",
    "campaignStatus": "active",
    "campaignMode": "per_subject",
    "campaignContactEntryId": "886313e1-3b8a-5372-9b90-0c9aee199e5d",
    "processingActivityVersionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "subject": {
      "referenceCode": "EST-04421",
      "subjectType": "student",
      "requestedForAgeBand": "under_14"
    },
    "status": "granted",
    "decisionActorType": "guardian",
    "requestedAt": "2026-08-20T12:00:00.000Z",
    "expiresAt": "2026-09-20T12:00:00.000Z",
    "updatedAt": "2026-08-25T14:03:21.000Z",
    "latestDecision": {
      "id": "0b8e4c2a-5d1f-4a6b-8c3e-7f9d2a1b6c4e",
      "decision": "granted",
      "decisionAuthority": "guardian",
      "captureMethod": "portal_link_otp",
      "decidedAt": "2026-08-25T14:03:21.000Z",
      "effectiveFrom": "2026-08-25T14:03:21.000Z",
      "effectiveTo": null,
      "revokedAt": null
    }
  }
}

Hoy el único evento con fan-out cruzado es consent.decision.recorded. Cualquier otro sourceType en una copia de audiencia partner no tiene proyección aprobada, y por diseño no se divulga nada: data llega null.


Suscripción comodín

El valor almacenado ["*"] suscribe el endpoint a todos los eventos, incluidos los que se agreguen al catálogo en el futuro. El comodín debe ir solo: enabledEvents acepta ["*"] o una lista de tipos explícitos, nunca una mezcla. Si omites enabledEvents al crear el endpoint — o envías una lista vacía — el valor almacenado es ["*"].


Requisitos y garantías de entrega

  • Solo HTTPS. La URL del endpoint debe usar https:// y no puede apuntar a hosts locales, direcciones privadas, link-local ni a la IP de metadatos de nube.
  • Secreto de una sola vista. Si no envías un secreto al crear el endpoint, el servidor genera uno (whsec_…) y lo devuelve solo en esa respuesta. Puedes rotarlo cuando quieras; la rotación también lo devuelve una única vez.
  • Reintentos. Una entrega se considera exitosa cuando tu endpoint responde con un estado 2xx. Si responde otra cosa o la conexión falla, el intento queda registrado como fallido y la cola vuelve a intentarlo; cada intento queda en el historial de entregas con su attemptNumber. Los destinos que ya recibieron con éxito una combinación (evento, origen, ocurrencia) no la vuelven a recibir en los reintentos.
  • Idempotencia. Cada transición distinta de un mismo recurso viaja con su propia occurrenceKey interna, pero un reintento puede hacer que veas el mismo evento más de una vez. Deduplica por la tupla event + sourceId + el contenido relevante de data (por ejemplo, el status en los eventos de entrega) y responde 2xx rápido — procesa en segundo plano si tu trabajo es lento.
  • Historial auditable. Cada intento queda en el registro de entregas del endpoint, consultable por la API de gestión: evento, origen, número de intento, estado y resumen del error (en un intento fallido, el resumen incluye el código con que respondió tu servidor). De la respuesta de tu servidor solo se conserva el código de estado, nunca el cuerpo.
  • Módulo requerido. El despacho exige el módulo integrations activo en la licencia de la organización.
  • Endpoints por conexión. Un endpoint puede quedar asociado a una conexión de partner mediante organizationPartnerConnectionId, para que el destino quede documentado junto a la relación que lo justifica.

¿Te sirvió esta página?