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.
Cambio en el esquema de firma. La firma HMAC-SHA-256 ya no se calcula
sobre el cuerpo a secas: ahora se calcula sobre la cadena
"{timestamp}.{rawBody}", donde timestamp es el valor de la cabecera
x-edugoverna-timestamp (segundos Unix) y rawBody son los bytes exactos del
cuerpo recibido. Si tu integración verificaba la firma del cuerpo solo, debes
actualizarla: las firmas antiguas ya no coinciden. El detalle está en
Verificación de la firma.
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 esx-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:
- Lee la cabecera
x-edugoverna-timestampy el cuerpo crudo de la solicitud (los bytes exactos recibidos — no re-serialices el JSON ya parseado). - Construye la cadena firmada como
${timestamp}.${rawBody}, con un punto literal entre el timestamp y el cuerpo. - Calcula
HMAC-SHA-256(secret, cadenaFirmada)en hexadecimal y compáralo con la cabecera de firma usando una comparación de tiempo constante. - 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_incidentoincident_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
expiresAtsin 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.sourceTypeesconsent_deliveryy la entrega usa el nuevo estado comooccurrenceKey, 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.
sourceTypeesconsent_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.
sourceTypeesconsent_delivery; se emite además delconsent.delivery.updatedcorrespondiente.
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 unoccurrenceKeyú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.
sourceTypeessecurity_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.
sourceTypeesincident_notificationy el nuevo estado viaja comooccurrenceKey.
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ásdeliveries[],latestDecision(con su desglose de opciones y evidencia) ylatestEvent. Si la solicitud pertenece a una campaña se agregancampaignId,campaignMode,campaignPublicSubjectFlow,campaignIdentityVerificationMode,campaignStatus,campaignContactEntryIdycampaignContactEntry.
- Nombre
consent_delivery- Tipo
- object
- Descripción
La fila de la entrega:
id,consentRequestId,channel,destinationMasked,status,deliveryProvider,providerMessageId, las marcas de tiempoqueuedAt/sentAt/deliveredAt/openedAt/failedAtyfailureReason.
- 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.
data puede llegar como null si el recurso de origen fue eliminado antes de
que la entrega se construyera, o si el evento no tiene una proyección aprobada
para la audiencia del endpoint (ver la sección siguiente). Trata data: null
como una señal de "consulta el recurso por la API usando sourceId".
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
occurrenceKeyinterna, pero un reintento puede hacer que veas el mismo evento más de una vez. Deduplica por la tuplaevent+sourceId+ el contenido relevante dedata(por ejemplo, elstatusen 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
integrationsactivo 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.