Campañas compartidas
Un colegio o un registrador externo puede compartir contigo un grupo de su padrón — un curso, un segmento, una generación — mediante un registro compartido (registry_share). Sobre esos grupos puedes lanzar campañas de consentimiento que viven en tu propio espacio de trabajo, mientras las solicitudes se crean en el padrón del dueño de los datos y Edugoverna hace todas las entregas. En esta página revisamos los endpoints de lectura de grupos compartidos, la creación de campañas y el envío por sobres.
Las rutas de esta página cuelgan de la URL base
https://app.edugoverna.com/api/partner/v1/organizations/:organizationId,
pero cuidado con qué organización va en la ruta: en GET /shares y GET /shares/:shareId/subjects, :organizationId es la organización dueña del
padrón (el colegio o registro). En todas las rutas /shared-campaigns,
:organizationId es tu propia organización de espacio de trabajo —
apuntarlas a la organización del dueño responde 403 PARTNER_WORKSPACE_REQUIRED. La guía paso a paso está en Guía de campañas
compartidas.
El modelo
- Registro compartido (share): la concesión con que el dueño te comparte un grupo. Tiene
groupKind(course,segmentocohort) y unmemberCount. El dueño puede revocarla en cualquier momento; una share revocada deja de resolver titulares. - Lectura ofuscada: los titulares de un grupo compartido se leen con obfuscación estructural — nombre, banda etaria, RUT enmascarado (
maskedRut), presencia de contactos (tipo y primario) y si una campaña alcanzaría a quien decide por el titular (consentReach), nunca los valores. Edugoverna despacha todas las entregas, así que tu integración no necesita el email ni el teléfono de nadie. La divulgación del RUT completo existe como excepción explícita y auditada (verdisclosure=full). - Campaña compartida: se crea y publica en una sola llamada sobre uno o más shares. En modo
bulk(por defecto) el roster completo se despacha al publicar; en modoper_subjectel roster se materializa como sobres (envelopes) — uno por familia — y tú los despachas uno a uno o por lotes, cada uno con su propia URL de destino. - Campaña declarada: cuando un registrador externo declara consentimientos que el colegio ya tiene firmados, esos consentimientos viven en una campaña de tu espacio de trabajo con
origin: "registrar_attestation". No envía mensajes, así que todo lo que la haría enviar está bloqueado con409 CONSENT_CAMPAIGN_ATTESTED_READ_ONLY: publicarla, reenviarla, los sobres, recuperar errores, sincronizar su audiencia y corregir destinos (redirects). Solo se puede editar su nombre, su descripción yendsAt; cambiar metadatos, recordatorios, canales o destinos responde el mismo409. Es una campañabulksin sobres, así que despachar o reenviar sobres responde también409 CONSENT_CAMPAIGN_DISPATCH_MODE_UNSUPPORTED. Cerrarla sí se permite. Ver Consentimiento declarado. - Destinos (redirects): al decidir, el firmante puede ser redirigido a tu aplicación.
redirectModeesauto(redirección inmediata) obridge(pantalla intermedia de Edugoverna). Los pares RUT→URL se pueden fijar al crear, al sincronizar audiencia o corregirse después; los RUT que no calcen con el padrón se ignoran en silencio — ninguna respuesta confirma cuáles calzaron. - Contrato de no divulgación: en campañas segmentadas por RUT, las respuestas nunca revelan cuántos ni cuáles RUT resolvieron a titulares reales. Los únicos números que se devuelven son los que tú mismo enviaste.
Listar registros compartidos
Devuelve los registros compartidos activos que esta organización dueña le concedió a tu cuenta de partner. :organizationId es la organización del dueño del padrón.
Un share es exactamente un grupo. Cuando el grupo es un curso (groupKind: "course"), course trae schoolYear, gradeLevel y courseCode (la letra o sección), si el dueño los cargó. Si no es un curso, course es null.
Permiso requerido: partner.shared_subjects.read · Alcance de conexión: shared_registries:read.
curl https://app.edugoverna.com/api/partner/v1/organizations/org_colegio/shares \
-H "Authorization: Bearer {keyId}.{secret}"
{
"shares": [
{
"id": "shr_1a2b3c",
"groupType": "segment",
"groupKind": "course",
"groupName": "5° Básico B",
"course": { "schoolYear": 2026, "gradeLevel": "5° Básico", "courseCode": "B" },
"memberCount": 34,
"grantSource": "school_dashboard",
"createdAt": "2026-03-15T10:00:00.000Z"
},
{
"id": "shr_4d5e6f",
"groupKind": "cohort",
"groupName": "Generación 2032"
// ...
}
]
}
Listar titulares de un grupo compartido
Devuelve una página de los titulares del grupo compartido, con la proyección ofuscada. :organizationId es la organización dueña; si la share no pertenece a ella, 404 REGISTRY_SHARE_NOT_FOUND; si fue revocada, la lectura se rechaza.
Permiso requerido: partner.shared_subjects.read · Alcance de conexión: shared_registries:read.
Con disclosure=full la respuesta agrega el campo rut con el RUT completo normalizado — lo que necesitas para cruzar el grupo contra tu propia base y segmentar por RUT. Requiere que el dueño tenga habilitada la divulgación de RUT para esa share (si la apagó, 403 SHARE_RUT_DISCLOSURE_DISABLED) y cada lectura ampliada queda auditada en la organización del dueño y en la tuya, con los titulares divulgados. Los valores de contacto siguen ocultos en cualquier modo; el único que puede salir es el correo del estudiante, de a un sobre y con consentimiento otorgado.
Cada titular trae enrollment: el curso del share (courseName, schoolYear, gradeLevel, courseCode), o null si el grupo no es un curso.
contacts son los canales propios del estudiante, y no dicen si una campaña lo alcanza: la solicitud va a quien decide, que bajo 14 años (bajo 16 si tu finalidad trata datos sensibles) es su apoderado. Eso lo dice consentReach, separado en general y sensitive porque entre 14 y 15 años difieren. Cada uno trae decidedBy (self, guardian, o null si falta la fecha de nacimiento), status (ok, guardian_missing, authority_undetermined) y channels: los canales de campaña (email, whatsapp) por los que se alcanza a alguien que puede decidir. Si el estudiante tiene varios apoderados con autoridad, la solicitud les llega a todos y decide el primero; basta con alcanzar a uno.
meta.missingContactCount cuenta, en todo el grupo, los titulares cuyo channels queda vacío en al menos uno de los dos tipos; meta.unreachableCount trae el mismo conteo separado ({ general, sensitive }). Con since, un contacto o apoderado nuevo no marca al titular como cambiado: vuelve a leer sin since para refrescar el alcance.
Parámetros de consulta opcionales
- Nombre
offset- Tipo
- integer
- Descripción
Desplazamiento de la página. Por defecto 0. Se ignora si mandas
cursor.
- Nombre
cursor- Tipo
- string
- Descripción
Paginación por llave: devuelve los titulares con
subjectIdmayor que éste (el orden es estable por id). Usa elmeta.nextCursorde la página anterior; esnullcuando no hay más.
- Nombre
since- Tipo
- string
- Descripción
ISO 8601. Sincronización incremental: sólo los titulares que entraron al grupo o cuya ficha cambió desde esa fecha, y en
removedlos que salieron ({ subjectId, removedAt }). Pasa elmeta.syncedAtde tu lectura anterior: es la hora del servidor tomada antes de leer, así que no pierdes lo que cambie mientras paginas ni dependes de tu reloj. Consince,totalCountcuenta sólo los cambios.
- Nombre
limit- Tipo
- integer
- Descripción
Tamaño de página, entre 1 y 200. Por defecto 50.
- Nombre
disclosure- Tipo
- string
- Descripción
fullpara la lectura ampliada con RUT completo.
- Nombre
include- Tipo
- string
- Descripción
nationalIdHash: cada titular traenationalIdPartnerHashen vez del RUT. Ver Hash de RUT por partner.
Hash de RUT por partner
Con include=nationalIdHash cada titular trae nationalIdPartnerHash: un HMAC del RUT con una clave propia de tu cuenta. El mismo RUT da el mismo hash para ti y otro distinto para cualquier otro partner, y rotar claves no lo cambia. Sirve para cruzar el grupo contra tu base sin guardar RUT en claro. Para hashear RUTs que tú tienes (por ejemplo, el que escribe un estudiante en tu página) está POST /v1/organizations/:organizationId/shares/:shareId/national-id-hash con { "ruts": [...] } (hasta 100); responde { "hashes": [...] }, con null para un RUT inválido, y consume el mismo presupuesto diario por share que la segmentación por RUT (429 SHARE_RUT_LOOKUP_BUDGET_EXCEEDED).
No hace falta disclosure=full, pero sí que el dueño mantenga habilitada la divulgación de RUT para esa share: aunque no revela el RUT, un hash comparable responde la misma pregunta de pertenencia —«¿este RUT está en el curso?»—, así que la decide el mismo interruptor. Si está apagado, ambas vías responden 403 SHARE_RUT_DISCLOSURE_DISABLED; en ese curso sincroniza con subjectId y maskedRut. Cada lectura queda auditada como divulgación.
curl -G https://app.edugoverna.com/api/partner/v1/organizations/org_colegio/shares/shr_1a2b3c/subjects \
-H "Authorization: Bearer {keyId}.{secret}" \
-d limit=50 \
-d disclosure=full
{
"share": {
"id": "shr_1a2b3c",
"groupType": "segment",
"groupKind": "course",
"groupName": "5° Básico B",
"memberCount": 34
},
"subjects": [
{
"subjectId": "9c1f6d1e-1c9d-4f6a-9a44-1f2ab5c0d9e1",
"displayName": "Martina Rojas Pino",
"subjectType": "student",
"currentAgeBand": "under_14",
"maskedRut": "**.***.435-2",
"rut": "25678435-2",
"contacts": [],
"consentReach": {
"general": { "decidedBy": "guardian", "status": "ok", "channels": ["email", "whatsapp"] },
"sensitive": { "decidedBy": "guardian", "status": "ok", "channels": ["email", "whatsapp"] }
},
"enrollment": {
"courseName": "5° Básico B",
"schoolYear": 2026,
"gradeLevel": "5° Básico",
"courseCode": "B"
}
}
],
"removed": [],
"meta": {
"totalCount": 34,
"offset": 0,
"limit": 50,
"missingContactCount": 3,
"unreachableCount": { "general": 3, "sensitive": 3 },
"nextCursor": null,
"since": null,
"syncedAt": "2026-10-01T14:03:11.402Z"
}
}
Consentimientos de un grupo compartido
Devuelve el estado de consentimiento de los titulares del grupo: una fila por solicitud de cualquiera de tus campañas sobre esta share —también las declaradas por el registrador, que no tienen sobres—. Es la otra mitad de listar titulares: cada fila trae subjectId, el mismo identificador, para cruzar las dos lecturas.
Cada fila es la misma proyección shared_partner que el webhook y el detalle por id, más subjectId, processingActivityCode, processingActivityVersionNumber e invalidatedAt. Nunca trae RUT, contactos ni nombre del titular, y de la decisión trae el rol que decidió (decisionAuthority) y cómo se capturó (captureMethod: portal_link_otp o registrar_attestation), nunca quién.
:organizationId es la organización dueña, como en el resto de las lecturas de shares. Una share de otra organización o de otro partner responde 404 REGISTRY_SHARE_NOT_FOUND; una revocada, 409 REGISTRY_SHARE_REVOKED.
Permiso requerido: partner.shared_subjects.read · Alcance de conexión: shared_registries:read.
Las filas se ordenan por updatedAt ascendente: para conciliar, guarda el mayor updatedAt que leíste y úsalo como el próximo updatedSince (inclusivo: deduplica por consentRequestId + updatedAt). Una solicitud reemplazada por otra campaña sigue saliendo, con invalidatedAt; quédate con la vigente de cada titular y actividad. En una campaña segmentada por RUT, una solicitud sin decisión no aparece hasta que el titular decide, y un titular suprimido en origen no aparece nunca.
Parámetros de consulta opcionales
- Nombre
processingActivityCode- Tipo
- string
- Descripción
Sólo las solicitudes de esa actividad tuya (todas sus versiones).
- Nombre
status- Tipo
- string
- Descripción
pending,granted,denied,revokedoexpired.
- Nombre
updatedSince- Tipo
- timestamp
- Descripción
Sólo las que cambiaron desde ese instante (ISO 8601, inclusivo).
- Nombre
limit- Tipo
- integer
- Descripción
Tamaño de página, entre 1 y 200. Por defecto 50.
- Nombre
cursor- Tipo
- string
- Descripción
meta.nextCursorde la página anterior. Firmado contra la share y los filtros: reutilizarlo con otros responde422 INVALID_CURSOR.
curl -G https://app.edugoverna.com/api/partner/v1/organizations/org_colegio/shares/shr_1a2b3c/consents \
-H "Authorization: Bearer {keyId}.{secret}" \
-d processingActivityCode=AULA_LEARNING \
-d status=granted
{
"share": { "id": "shr_1a2b3c" },
"consents": [
{
"projection": "shared_partner",
"consentRequestId": "c4a1b2d3-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"dataOwnerOrganizationId": "org_colegio",
"registryShareId": "shr_1a2b3c",
"campaignId": "03aa680e-f976-4e51-b58a-6944d78a9448",
"campaignStatus": "published",
"campaignMode": "targeted",
"campaignOrigin": "registrar_attestation",
"campaignContactEntryId": "6f0e2d1c-4b3a-4c2d-9e8f-7a6b5c4d3e2f",
"partnerExternalId": null,
"processingActivityVersionId": "ee13e852-ce7a-4fde-9c6c-9cc8a06f02d0",
"processingActivityCode": "AULA_LEARNING",
"processingActivityVersionNumber": 1,
"subjectId": "9c1f6d1e-1c9d-4f6a-9a44-1f2ab5c0d9e1",
"subject": {
"referenceCode": "EST-0042",
"subjectType": "student",
"requestedForAgeBand": "under_14"
},
"status": "granted",
"decisionActorType": "guardian",
"requestedAt": "2026-10-15T20:26:02.914Z",
"expiresAt": null,
"invalidatedAt": null,
"updatedAt": "2026-10-15T20:26:03.120Z",
"latestDecision": {
"id": "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c0d",
"decision": "granted",
"decisionAuthority": "guardian",
"captureMethod": "registrar_attestation",
"decidedAt": "2026-10-15T20:26:02.914Z",
"effectiveFrom": "2026-10-15T20:26:02.914Z",
"effectiveTo": null,
"revokedAt": null,
"evidence": { "id": "a7b8c9d0-…:manifest", "checksum": "f542639105f83d66…" }
}
}
],
"meta": { "nextCursor": null, "syncedAt": "2026-10-15T21:00:00.000Z" }
}
Crear una campaña compartida
Crea y publica una campaña de consentimiento sobre uno o más registros compartidos. :organizationId es tu organización de espacio de trabajo; cada share del cuerpo se re-autoriza contra tu cuenta de partner. Responde 202 Accepted con una forma idéntica resuelva lo que resuelva la audiencia — el contrato de no divulgación.
Permiso requerido: partner.shared_campaigns.write · Alcance de conexión: consents:write.
Cada target apunta a un share y, opcionalmente, a una lista de RUTs dentro del grupo (segmentación por RUT, hasta 500 por share y 1000 por campaña). Sin ruts, la campaña cubre el grupo completo (modo group); con ruts, sólo los que calcen por índice ciego (modo rut, con proyección de no divulgación de membresía).
Las URLs de redirección son tuyas y no llevan lista blanca en este flujo: en una campaña compartida el aviso que el titular lee lo firma tu organización, y el dueño lo aceptó al conectarse.
Atributos requeridos
- Nombre
processingActivityVersionId- Tipo
- string
- Descripción
UUID de la versión de tu actividad de tratamiento — usa la versión publicada vigente: es el aviso que el titular leerá al decidir.
- Nombre
name- Tipo
- string
- Descripción
Nombre de la campaña (2–160).
- Nombre
targets- Tipo
- array
- Descripción
Entre 1 y 50 entradas
{ shareId, ruts? }. Máximo 1000 RUTs sumados.
Atributos opcionales
- Nombre
description- Tipo
- string
- Descripción
Descripción (máx. 2000).
- Nombre
channels- Tipo
- array
- Descripción
["email"],["whatsapp"]o ambos. Por defecto["email"].whatsappsale desde el número de tu propio espacio de trabajo (no del colegio) y exige que lo tengas conectado en Ajustes → WhatsApp con mensajes aprobados por Meta; si no, la llamada responde409 WHATSAPP_CHANNEL_UNAVAILABLEcondetails.reason(not_licensed,not_connected,setup_pending,setup_failed,disconnected,account_restricted,not_funded,templates_pendingospend_limit_reached).
- Nombre
endsAt- Tipo
- timestamp
- Descripción
Cierre de la campaña (ISO 8601, futuro).
- Nombre
reminderDayOffsets- Tipo
- array
- Descripción
Hasta 3 recordatorios en días desde el envío (1–30), estrictamente crecientes.
- Nombre
dispatchMode- Tipo
- string
- Descripción
bulk(por defecto: despacha el roster completo al publicar) oper_subject(materializa sobres y espera tus envíos uno a uno).
- Nombre
defaultSuccessRedirectUrl- Tipo
- string
- Descripción
URL de éxito por defecto para toda la campaña.
- Nombre
defaultDeclineRedirectUrl- Tipo
- string
- Descripción
URL de rechazo por defecto.
- Nombre
redirectMode- Tipo
- string
- Descripción
autoobridge.
- Nombre
redirects- Tipo
- array
- Descripción
Hasta 2500 pares decididos al crear. Cada par nombra al estudiante por uno de
rutosubjectId(el id de la lectura del grupo) y fija sólo lo que trae:successRedirectUrl,declineRedirectUrl,redirectMode,studentRedirectUrl(el enlace del aviso al estudiante) ypartnerExternalId(tu correlativo, que luego viaja en webhooks, eneg_tokeny en la lista de sobres). Quedan en el sobre de cada estudiante y cualquier vía de completación resuelve lo mismo. Los RUT o ids que no calcen se ignoran en silencio.
- Nombre
studentNotification- Tipo
- object
- Descripción
{ enabled, urlTemplate? }. Conenabled: true, cuando decide el apoderado y otorga, EduGoverna le escribe al estudiante a su correo registrado, con un texto propio ("Tu apoderado/a autorizó tu participación en …", con el nombre de su colegio y de la campaña) y tu enlace para continuar. El enlace es elstudentRedirectUrldel sobre, ourlTemplatecon{envelopeId}y{partnerExternalId}reemplazados. Sin correo del estudiante no se envía nada y la decisión no se ve afectada. En ambos casos recibesconsent.student_notifiedconcontext.outcome(sentono_contact). El apoderado sigue aterrizando ensuccessRedirectUrl.
- Nombre
publicCompletionEnabled- Tipo
- boolean
- Descripción
Abre la puerta pública de completación: un titular del roster sin canal registrado puede identificarse con su RUT, verificar un contacto propio con OTP y firmar su propio sobre. La respuesta incluye
publicCompletionUrl.
curl https://app.edugoverna.com/api/partner/v1/organizations/org_milectura/shared-campaigns \
-H "Authorization: Bearer {keyId}.{secret}" \
-H "Content-Type: application/json" \
-d '{
"processingActivityVersionId": "3f2e1d0c-9b8a-4765-b432-1a0f9e8d7c6b",
"name": "Alta 5° Básico B — Colegio San Martín",
"dispatchMode": "per_subject",
"defaultSuccessRedirectUrl": "https://app.milectura.cl/bienvenida",
"redirectMode": "bridge",
"publicCompletionEnabled": true,
"targets": [{ "shareId": "shr_1a2b3c" }]
}'
{
"campaignId": "a9b8c7d6-e5f4-4321-a0b1-c2d3e4f5a6b7",
"submittedShareCount": 1,
"publicCompletionEnabled": true,
"publicCompletionUrl": "https://app.edugoverna.com/consents/campaigns/…",
"status": "accepted"
}
Obtener una campaña compartida
Devuelve el estado agregado de la campaña: el embudo de desenlaces (outcomes) y — en campañas por grupo — el total del roster y su desglose de cobertura. En campañas segmentadas por RUT la respuesta trae decidedOnly: true y omite denominadores, de nuevo por el contrato de no divulgación: sólo se describen los contactos que ya decidieron.
Permiso requerido: partner.consents.read · Alcance de conexión: consents:read.
- Nombre
origin- Tipo
- string
- Descripción
operator(una campaña que lanzaste tú) oregistrar_attestation(los consentimientos que un registrador externo declaró firmados ante el colegio; no envía mensajes, así que no admite envío ni cambios de canales o destinos: ver El modelo).
- Nombre
outcomes- Tipo
- object
- Descripción
counts(granted,denied,revoked,expired,cancelled,errored),inProgressytotal.
- Nombre
coverage- Tipo
- object
- Descripción
Sólo en modo grupo:
totalStudents,deliverableStudents,manualDeliveryStudents,authorityUndetermined,guardianMissingy, en modo sobre,awaitingSendStudents.
curl https://app.edugoverna.com/api/partner/v1/organizations/org_milectura/shared-campaigns/a9b8c7d6-e5f4-4321-a0b1-c2d3e4f5a6b7 \
-H "Authorization: Bearer {keyId}.{secret}"
{
"campaignId": "a9b8c7d6-e5f4-4321-a0b1-c2d3e4f5a6b7",
"name": "Alta 5° Básico B — Colegio San Martín",
"status": "published",
"origin": "operator",
"dispatchMode": "per_subject",
"targetingMode": "group",
"channels": ["email"],
"publishedAt": "2026-08-25T12:30:00.000Z",
"closedAt": null,
"endsAt": null,
"publicCompletionEnabled": true,
"publicCompletionUrl": "https://app.edugoverna.com/consents/campaigns/…",
"outcomes": {
"counts": {
"granted": 12,
"denied": 1,
"revoked": 0,
"expired": 0,
"cancelled": 0,
"errored": 0
},
"inProgress": 21,
"total": 34
},
"rosterTotal": 34,
"coverage": {
"totalStudents": 34,
"deliverableStudents": 13,
"manualDeliveryStudents": 0,
"authorityUndetermined": 0,
"guardianMissing": 0,
"awaitingSendStudents": 21
}
}
Cambiar el cierre de una campaña
Extiende o adelanta el endsAt de una campaña, también ya publicada. Las solicitudes pendientes pasan a vencer en la fecha nueva, incluidas las que viven en el padrón de cada colegio. Las que ya vencieron no reviven: para pedirlas de nuevo dentro del plazo nuevo está el reenvío del sobre. Los sobres que se despachen después toman el endsAt nuevo.
Permiso requerido: partner.shared_campaigns.write · Alcance de conexión: consents:write.
La fecha debe ser futura (422 VALIDATION_ERROR) y la campaña no puede estar cerrada (409 CONSENT_CAMPAIGN_CLOSED). Cada cambio deja el evento de auditoría updated con el endsAt nuevo y cuántas solicitudes se reprogramaron.
Solo actúa sobre campañas compartidas. Una campaña de tu workspace creada con audiencia en línea responde 409 PARTNER_CAMPAIGN_FAMILY_MISMATCH y se gestiona con PATCH /consents/campaigns/:campaignId.
La respuesta trae origin (operator o registrar_attestation), el mismo valor de la lectura de la campaña. En una campaña declarada (registrar_attestation) esta es una de las pocas ediciones permitidas, junto al nombre y la descripción.
Adelantar el cierre corta los recordatorios y el portal al llegar la fecha nueva, pero no es inmediato y la campaña sigue published. Para terminarla ya, ciérrala.
- Nombre
endsAt- Tipo
- timestamp
- Descripción
Requerido. ISO 8601 con zona horaria.
curl -X PATCH https://app.edugoverna.com/api/partner/v1/organizations/org_milectura/shared-campaigns/a9b8c7d6-e5f4-4321-a0b1-c2d3e4f5a6b7 \
-H "Authorization: Bearer {keyId}.{secret}" \
-H "Content-Type: application/json" \
-d '{ "endsAt": "2026-11-15T03:00:00Z" }'
{
"campaignId": "a9b8c7d6-e5f4-4321-a0b1-c2d3e4f5a6b7",
"status": "published",
"endsAt": "2026-11-15T03:00:00.000Z",
"origin": "operator"
}
Cerrar una campaña compartida
Cierra la campaña de inmediato, con la misma cascada que el botón «Cerrar campaña» de la Consola, también sobre las solicitudes que viven en el padrón de cada colegio:
- las solicitudes pendientes pasan a
invalidated(invalidationReason: "campaign_closed") y cada una emiteconsent.invalidated— al colegio y, con la proyección para partners, a ti; - sus enlaces dejan de abrir el formulario, no salen más recordatorios y la completación pública se cierra;
- los sobres que nunca se despacharon quedan cancelados, y despachar o reenviar un sobre responde
409 CONSENT_CAMPAIGN_CLOSED; - la campaña queda
closedy ya no se puede editar.
Las decisiones ya registradas no se tocan: quien otorgó o rechazó conserva su comprobante y la evidencia sigue disponible; un consentimiento otorgado sigue vigente hasta que se revoque. Si el titular te pide el retiro a ti, regístralo con POST /consents/requests/:consentRequestId/revoke sobre tu workspace. Cerrar no se deshace.
Permiso requerido: partner.shared_campaigns.write · Alcance de conexión: consents:write. La organización de la URL es tu workspace (403 PARTNER_WORKSPACE_REQUIRED). Una campaña con audiencia en línea responde 409 PARTNER_CAMPAIGN_FAMILY_MISMATCH y se cierra con POST /consents/campaigns/:campaignId/close. Cerrar una campaña ya cerrada responde 409 CONSENT_CAMPAIGN_ALREADY_CLOSED, así que un reintento después de un timeout se reconoce por ese código.
La respuesta trae también origin, para que una campaña declarada no se confunda con una lanzada por ti; cerrar una campaña declarada está permitido. El cuerpo es opcional. La respuesta no trae conteos —en una campaña segmentada por RUT dirían cuántos de los RUT enviados estaban en el padrón—; cada solicitud anulada te llega por su propio consent.invalidated. En una campaña segmentada por RUT no recibes ninguno: solo se anulan solicitudes sin decisión, y eso es justo lo que ese modo no revela.
- Nombre
reason- Tipo
- string
- Descripción
Opcional (1–500). Motivo del cierre; queda en el evento de auditoría
closed. Nunca se le muestra al titular.
curl -X POST https://app.edugoverna.com/api/partner/v1/organizations/org_milectura/shared-campaigns/a9b8c7d6-e5f4-4321-a0b1-c2d3e4f5a6b7/close \
-H "Authorization: Bearer {keyId}.{secret}" \
-H "Content-Type: application/json" \
-d '{ "reason": "Proceso de admisión cerrado" }'
{
"campaignId": "a9b8c7d6-e5f4-4321-a0b1-c2d3e4f5a6b7",
"status": "closed",
"closedAt": "2026-10-02T14:03:22.511Z",
"origin": "operator"
}
Listar sobres de una campaña
Devuelve el roster de la campaña sobre a sobre, con el estado de cada uno y su destino planificado. Es tu tablero de seguimiento en modo per_subject. En campañas segmentadas por RUT esta lectura se rechaza con 409 CONSENT_CAMPAIGN_RUT_MODE_RESTRICTED — el estado por titular revelaría membresía.
Permiso requerido: partner.consents.read · Alcance de conexión: consents:read.
El campo completedVia de un sobre completado distingue dispatch (firmó por su enlace despachado) de public_completion (firmó por la puerta pública; en ese caso reviewStatus refleja la revisión del staff: pending, verified o rejected).
Parámetros de consulta opcionales
- Nombre
status- Tipo
- string
- Descripción
Filtra por estado del sobre (por ejemplo
awaiting_send,queued,completed,error).
- Nombre
since- Tipo
- string
- Descripción
ISO 8601. Sólo los sobres que cambiaron desde esa fecha: el sobre o su solicitud (una decisión, una revocación, una expiración). Cada sobre trae su
updatedAt. Usa elmeta.syncedAtde una lectura comosincede la siguiente: se toma antes de leer, así que no se pierde nada que cambie mientras paginas.
- Nombre
limit- Tipo
- integer
- Descripción
Tamaño de página, hasta 500. Sin
limitse devuelve todo el roster, como siempre.
- Nombre
cursor- Tipo
- string
- Descripción
Paginación por llave (orden estable por
envelopeId): pasa elmeta.nextCursorde la página anterior; esnullcuando no hay más.
curl -G https://app.edugoverna.com/api/partner/v1/organizations/org_milectura/shared-campaigns/a9b8c7d6-e5f4-4321-a0b1-c2d3e4f5a6b7/envelopes \
-H "Authorization: Bearer {keyId}.{secret}" \
-d status=awaiting_send
{
"campaignId": "a9b8c7d6-e5f4-4321-a0b1-c2d3e4f5a6b7",
"envelopes": [
{
"envelopeId": "env_7f8e9d",
"subjectId": "9c1f6d1e-1c9d-4f6a-9a44-1f2ab5c0d9e1",
"referenceCode": null,
"partnerExternalId": null,
"status": "awaiting_send",
"errorCode": null,
"errorMessage": null,
"consentRequestId": null,
"requestStatus": null,
"sentAt": null,
"successRedirectUrl": null,
"plannedSuccessRedirectUrl": "https://app.milectura.cl/bienvenida?e=88231",
"completedVia": null,
"reviewStatus": null
}
]
}
Despachar sobres
Despacha uno o varios sobres de una campaña per_subject publicada, cada uno con su propia URL de destino. Acepta un sobre (el cuerpo es el objeto directamente) o un lote ({ "envelopes": [...] }, hasta 200). Responde 202 con el resultado por sobre.
Permiso requerido: partner.shared_campaigns.write · Alcance de conexión: consents:write.
Cada sobre identifica al estudiante por exactamente uno de subjectId (el ID de la lectura del grupo) o rut. Los estados posibles del resultado:
sent— se creó la solicitud y se encoló su entrega (webhookconsent.requesteden la organización del titular). Trae ademásportalAccessLinks: el token del portal por entrega, para que muestres el enlace dentro de tu app sin esperar a que el apoderado abra el correo.already_sent— el sobre ya tenía solicitud; se devuelve la existente. Reintentar es seguro: hay una solicitud por sobre, garantizada por esquema.already_granted— el titular ya otorgó esta versión de la actividad en otra campaña: no se le pide de nuevo ni se envía nada, yconsentRequestIdes el de ese consentimiento.not_in_campaign— el titular no está en el roster. En un envío individual esto es404 CONSENT_CAMPAIGN_CONTACT_NOT_FOUND; en un lote, cada fila lleva su propio desenlace.share_revoked— el dueño revocó la share; el sobre no se envía.authority_unresolved— todavía nadie puede decidir por este titular:errorCodedice por qué (CONSENT_AUTHORITY_UNDETERMINEDsi falta la fecha de nacimiento,GUARDIAN_AUTHORITY_REQUIREDsi no hay apoderado vigente,SUBJECT_PROCESSING_OPPOSEDsi el titular se opuso a esta finalidad). El resto del lote se envía igual. La autoridad se re-evalúa en cada despacho: cuando el colegio corrija el padrón, reenvía el mismo sobre. En un envío individual esto es un409con ese mismo código.
Requisitos de la campaña: publicada (409 CONSENT_CAMPAIGN_NOT_PUBLISHED), no cerrada (409 CONSENT_CAMPAIGN_CLOSED), modo per_subject (409 CONSENT_CAMPAIGN_DISPATCH_MODE_UNSUPPORTED) y no segmentada por RUT. Cada despacho deja el evento de auditoría envelopes_dispatched con los conteos del lote.
Atributos por sobre
- Nombre
subjectId- Tipo
- string
- Descripción
UUID del titular (excluyente con
rut).
- Nombre
rut- Tipo
- string
- Descripción
RUT del estudiante (excluyente con
subjectId).
- Nombre
successRedirectUrl- Tipo
- string
- Descripción
URL de destino tras otorgar; prevalece sobre la planificada.
- Nombre
declineRedirectUrl- Tipo
- string
- Descripción
URL de destino tras denegar.
- Nombre
redirectMode- Tipo
- string
- Descripción
autoobridge.
- Nombre
partnerExternalId- Tipo
- string
- Descripción
Tu correlativo para este sobre (1–160). Queda en el sobre y viaja en webhooks, en
eg_tokeny en la lista de sobres.
- Nombre
studentRedirectUrl- Tipo
- string
- Descripción
Tu enlace para el aviso al estudiante de este sobre, si la campaña tiene
studentNotificationactivo. Prevalece sobreurlTemplate.
- Nombre
idempotencyKey- Tipo
- string
- Descripción
Llave de idempotencia (8–120).
curl https://app.edugoverna.com/api/partner/v1/organizations/org_milectura/shared-campaigns/a9b8c7d6-e5f4-4321-a0b1-c2d3e4f5a6b7/envelopes \
-H "Authorization: Bearer {keyId}.{secret}" \
-H "Content-Type: application/json" \
-d '{
"rut": "25678435-2",
"successRedirectUrl": "https://app.milectura.cl/bienvenida?e=88231",
"partnerExternalId": "lic-88231"
}'
{
"subjectId": "9c1f6d1e-1c9d-4f6a-9a44-1f2ab5c0d9e1",
"rut": "25678435-2",
"envelopeId": "env_7f8e9d",
"consentRequestId": "c4a1b2d3-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"status": "sent",
"portalAccessLinks": [
{
"consentDeliveryId": "d1e2f3a4-b5c6-4d7e-8f90-a1b2c3d4e5f6",
"recipientSubjectId": "5d2a8c3b-77e1-4b0e-9f1d-3c4e5f6a7b8c",
"channel": "email",
"token": "d1e2f3a4-b5c6-4d7e-8f90-a1b2c3d4e5f6.…"
}
]
}
Reenviar un sobre
Reenvía la entrega de un sobre, opcionalmente con un destino nuevo. Hay una solicitud por sobre a la vez, así que un reenvío con URL distinta no crea una segunda solicitud: actualiza el destino de la existente y re-encola su entrega — el titular recibe el mismo enlace de siempre, ahora apuntando al destino corregido. Si el sobre nunca se había enviado, esta llamada equivale a su primer despacho. Responde 202 con el resultado del sobre.
Solicitud vencida. Si la solicitud del sobre venció sin respuesta y la campaña sigue en fecha (por ejemplo, porque se extendió su endsAt), el reenvío emite una solicitud nueva para el mismo titular, con el destino de la anterior salvo que mandes otro. La respuesta trae el consentRequestId nuevo, sus portalAccessLinks y previousConsentRequestId; la vencida sigue en la campaña con su historial. Una decisión ya tomada no se vuelve a pedir: quien dijo que no puede cambiar de opinión en el portal de transparencia.
Sin nada que reenviar la respuesta es status: "not_resendable" con reason, nunca sent:
already_decided— la solicitud ya tiene decisión (otorgada, rechazada o revocada) o se anuló.campaign_ended— venció junto con la campaña; no hay plazo para pedirla de nuevo.nothing_queued— sigue vigente, pero ninguna entrega quedó en cola (quien decide no tiene un canal utilizable).
Permiso requerido: partner.shared_campaigns.write · Alcance de conexión: consents:write.
Un :envelopeId que no pertenece a la campaña responde 404 CONSENT_CAMPAIGN_CONTACT_NOT_FOUND.
La respuesta trae portalAccessLinks con el token vigente de cada entrega reenviada — el mismo que recibe el apoderado: se conserva mientras siga válido y se acuña uno nuevo si venció.
Atributos opcionales
- Nombre
successRedirectUrl- Tipo
- string
- Descripción
Nuevo destino tras otorgar.
- Nombre
declineRedirectUrl- Tipo
- string
- Descripción
Nuevo destino tras denegar.
- Nombre
redirectMode- Tipo
- string
- Descripción
autoobridge.
curl https://app.edugoverna.com/api/partner/v1/organizations/org_milectura/shared-campaigns/a9b8c7d6-e5f4-4321-a0b1-c2d3e4f5a6b7/envelopes/env_7f8e9d/resend \
-H "Authorization: Bearer {keyId}.{secret}" \
-H "Content-Type: application/json" \
-d '{ "successRedirectUrl": "https://app.milectura.cl/bienvenida?e=88231&retry=1" }'
{
"subjectId": "9c1f6d1e-1c9d-4f6a-9a44-1f2ab5c0d9e1",
"rut": null,
"envelopeId": "env_7f8e9d",
"consentRequestId": "c4a1b2d3-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"status": "sent",
"portalAccessLinks": [
{
"consentDeliveryId": "d1e2f3a4-b5c6-4d7e-8f90-a1b2c3d4e5f6",
"recipientSubjectId": "5d2a8c3b-77e1-4b0e-9f1d-3c4e5f6a7b8c",
"channel": "email",
"token": "d1e2f3a4-b5c6-4d7e-8f90-a1b2c3d4e5f6.…"
}
]
}
Despachar sobres por lote asíncrono
Para más de 200 sobres: hasta 5.000 en una llamada. Responde 202 con el job. Edugoverna los despacha en tramos de 200 con el mismo proceso que el envío síncrono, así que cada sobre tiene los mismos desenlaces (sent, already_sent, authority_unresolved, …) y reintentar es igual de seguro. Al terminar recibes envelopes.job.completed con los conteos.
Permiso requerido: partner.shared_campaigns.write · Alcance de conexión: consents:write.
Mandar la cabecera Idempotency-Key hace que repetir la llamada devuelva el mismo job en vez de crear otro. Los requisitos de la campaña (publicada, per_subject, no segmentada por RUT) se validan al crear el job, no a mitad de camino. Si la campaña se cierra mientras corre, el job termina en failed con errorCode.
GET …/envelope-jobs/:jobId devuelve el estado (queued, running, completed o failed), los conteos y los resultados por fila en el orden en que los mandaste (position), paginados con cursor y limit (hasta 500).
{
"jobId": "7a3f…",
"campaignId": "a9b8c7d6-e5f4-4321-a0b1-c2d3e4f5a6b7",
"status": "completed",
"totalCount": 3000,
"processedCount": 3000,
"sentCount": 2994,
"authorityUnresolvedCount": 6,
"errorCode": null,
"createdAt": "2026-09-25T12:00:00.000Z",
"completedAt": "2026-09-25T12:04:10.000Z",
"results": [
{
"position": 0,
"subjectId": "9c1f6d1e-1c9d-4f6a-9a44-1f2ab5c0d9e1",
"envelopeId": "886313e1-3b8a-5372-9b90-0c9aee199e5d",
"consentRequestId": "c4a1b2d3-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"status": "sent"
}
],
"meta": { "nextCursor": 199 }
}
Correo del estudiante
El correo primario del estudiante de un sobre, para que le escribas sobre lo que su familia autorizó. Nunca el de un apoderado, y nunca un teléfono. Se lee de a un sobre. Responde { envelopeId, email } con Cache-Control: no-store.
Permiso requerido: partner.shared_subjects.read · Alcance de conexión: shared_registries:read.
Condiciones, todas a la vez:
- El dueño del padrón habilitó la entrega en el share (apagada por defecto; el colegio la enciende en su consola y el registrador en su portal). Si no,
403 SHARE_STUDENT_EMAIL_DISCLOSURE_DISABLED. - El consentimiento del sobre está otorgado y vigente. Sin eso, o si el sobre no existe o el estudiante no tiene correo, responde
404 CONSENT_ENVELOPE_STUDENT_CONTACT_NOT_AVAILABLE, sin distinguir entre los casos.
Cada lectura (y cada negativa) deja una fila de divulgación en la Auditoría del dueño y otra en la tuya.
Uso transaccional, sin registro. El correo es para el envío que motivó la lectura: no lo guardes en tu base ni lo copies a otros sistemas. Vuelve a leerlo cada vez que lo necesites. Si el consentimiento se revoca (consent.revoked), deja de estar disponible y no debes conservar ninguna copia.
{
"envelopeId": "886313e1-3b8a-5372-9b90-0c9aee199e5d",
"email": "estudiante@colegio.cl"
}
Evidencia de un sobre
La prueba del consentimiento de un sobre ya decidido, para exhibirla ante un apoderado o un fiscalizador sin pedírsela al colegio. No trae datos de quien decidió, solo el rol (decisionAuthority). Qué se decidió, cuándo, sobre qué versión del aviso (notice.noticeChecksum), y el id, tipo y checksum de cada artefacto sellado: el manifiesto firmado (sealed_manifest) y sus recibos. Cada artefacto trae su verificationUrl.
Permiso requerido: partner.consents.read · Alcance de conexión: consents:read.
Un sobre sin decisión responde 404 CONSENT_ENVELOPE_DECISION_NOT_FOUND, igual que uno que no existe.
captureMethod es portal_link_otp cuando la familia decidió con su enlace y código, y registrar_attestation cuando el consentimiento fue declarado por el establecimiento. En ese caso el manifiesto cita la declaración jurada del registrador (registrarDeclaration) y el colegio conserva los originales firmados: no es una firma con código de un solo uso.
Verificar un checksum
GET /api/public/evidence/verify?checksum=<sha256> es público y no revela de quién es la evidencia. Responde { "valid": false } o { "valid": true, evidenceType, capturedAt, signedAt, algorithm, kid }. EduGoverna re-hashea el artefacto guardado y verifica la firma Ed25519 del manifiesto con la clave publicada en /api/public/jwks.json, no con la que trae el propio documento. El mismo checksum llega en los webhooks (latestDecision.evidence).
{
"envelopeId": "886313e1-3b8a-5372-9b90-0c9aee199e5d",
"consentRequestId": "c4a1b2d3-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"requestStatus": "granted",
"decision": "granted",
"decisionAuthority": "guardian",
"captureMethod": "portal_link_otp",
"decidedAt": "2026-09-25T14:03:21.000Z",
"revokedAt": null,
"notice": {
"processingActivityVersionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"versionNumber": 3,
"noticeChecksum": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
},
"evidence": [
{
"id": "…:manifest",
"type": "sealed_manifest",
"checksum": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b",
"algorithm": "Ed25519",
"kid": "v1",
"capturedAt": "2026-09-25T14:03:21.000Z",
"verificationUrl": "https://app.edugoverna.com/api/public/evidence/verify?checksum=3a7b…4f1b"
}
]
}
Sincronizar la audiencia
La segunda ola: cuando el dueño del padrón agrega estudiantes nuevos a un curso ya compartido, esta llamada re-resuelve los grupos guardados de la campaña contra el registro de hoy y agrega sólo los contactos faltantes. En una campaña bulk publicada, además emite y despacha sus solicitudes por tramos; en modo per_subject, los nuevos quedan awaiting_send para que tú los despaches. Responde 202.
Permiso requerido: partner.shared_campaigns.write · Alcance de conexión: consents:write.
Sobre una campaña declarada (origin: "registrar_attestation") responde 409 CONSENT_CAMPAIGN_ATTESTED_READ_ONLY: agregaría al curso entero, también a quienes el colegio no marcó, y les enviaría la solicitud.
El comando es repetible por diferencia: si pendingRequestCount vuelve mayor que cero, quedan solicitudes por emitir de esta ola — vuelve a llamar hasta que llegue a cero. Los redirects del cuerpo aplican sólo a los contactos que esta pasada agrega; para corregir destinos de sobres ya existentes está el endpoint de redirects.
Atributos opcionales
- Nombre
redirects- Tipo
- array
- Descripción
Hasta 2500 pares
{ rut, successRedirectUrl, declineRedirectUrl?, redirectMode? }para los contactos nuevos de esta ola.
curl https://app.edugoverna.com/api/partner/v1/organizations/org_milectura/shared-campaigns/a9b8c7d6-e5f4-4321-a0b1-c2d3e4f5a6b7/sync-audience \
-H "Authorization: Bearer {keyId}.{secret}" \
-H "Content-Type: application/json" \
-d '{
"redirects": [
{ "rut": "26011822-5", "successRedirectUrl": "https://app.milectura.cl/bienvenida?e=90112" }
]
}'
{
"addedContactCount": 2,
"materializedRequestCount": 0,
"pendingRequestCount": 0,
"submittedRedirectCount": 1
}
Corregir destinos planificados
Re-escribe las URLs planificadas de los sobres que aún no tienen solicitud (por ejemplo, los awaiting_send de una campaña per_subject, o los pendientes de una bulk aún en materialización). Un sobre ya despachado no cambia retroactivamente por esta vía — para ese existe el reenvío con destino nuevo.
Permiso requerido: partner.shared_campaigns.write · Alcance de conexión: consents:write.
Sólo disponible en campañas por grupo (no segmentadas por RUT) y no cerradas (409 CONSENT_CAMPAIGN_CLOSED). Por cada RUT gana el último par de la lista; los RUT que no calcen se ignoran en silencio.
Atributos requeridos
- Nombre
redirects- Tipo
- array
- Descripción
Entre 1 y 2500 pares
{ rut, successRedirectUrl, declineRedirectUrl?, redirectMode? }.
curl https://app.edugoverna.com/api/partner/v1/organizations/org_milectura/shared-campaigns/a9b8c7d6-e5f4-4321-a0b1-c2d3e4f5a6b7/redirects \
-H "Authorization: Bearer {keyId}.{secret}" \
-H "Content-Type: application/json" \
-d '{
"redirects": [
{ "rut": "25678435-2", "successRedirectUrl": "https://app.milectura.cl/bienvenida?e=88231-b" }
]
}'
{
"submittedRedirectCount": 1,
"updatedContactCount": 1
}
Verificar el redirect (eg_token)
Cuando una solicitud viaja por una conexión con tu cuenta, el redirect posterior a la decisión agrega eg_token a tu URL, además de los consent_id y decision de siempre (esos siguen en claro y sin firma, así que no les creas por sí solos). eg_token es un JWS compacto EdDSA (Ed25519) que puedes verificar sin llamar a la API:
- Lee el
kiddel encabezado (typ: "eg-redirect+jwt") y toma esa clave deGET https://app.edugoverna.com/api/public/jwks.json. Puedes cachear el JWKS; si aparece unkiddesconocido, vuelve a pedirlo. - Verifica la firma y exige
iss = "edugoverna",aud =tupartnerAccountIdyexpen el futuro (vive 10 minutos; tolera un par de minutos de desfase de reloj). - Usa las claims:
consentRequestId,envelopeId,partnerExternalId,decision,decisionAuthority,decisionActorType,decidedAtyjti(único por token, útil para rechazar reusos).
Cambiar cualquier parámetro invalida la firma. Sin eg_token (una solicitud que no viaja por tu conexión, o un redirect más viejo que 10 minutos), consulta el estado por API o espera el webhook.
import { createRemoteJWKSet, jwtVerify } from 'jose'
const jwks = createRemoteJWKSet(
new URL('https://app.edugoverna.com/api/public/jwks.json'),
)
const { payload } = await jwtVerify(egToken, jwks, {
issuer: 'edugoverna',
audience: partnerAccountId,
typ: 'eg-redirect+jwt',
clockTolerance: 120,
})
// payload.envelopeId, payload.decision, payload.decisionAuthority, …