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.

El modelo

  • Registro compartido (share): la concesión con que el dueño te comparte un grupo. Tiene groupKind (course, segment o cohort) y un memberCount. 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 (ver disclosure=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 modo per_subject el 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 con 409 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 y endsAt; cambiar metadatos, recordatorios, canales o destinos responde el mismo 409. Es una campaña bulk sin sobres, así que despachar o reenviar sobres responde también 409 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. redirectMode es auto (redirección inmediata) o bridge (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.

GET/v1/organizations/:organizationId/shares

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.

Request
GET
/v1/.../shares
curl https://app.edugoverna.com/api/partner/v1/organizations/org_colegio/shares \
  -H "Authorization: Bearer {keyId}.{secret}"
Response
{
  "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"
      // ...
    }
  ]
}

GET/v1/organizations/:organizationId/shares/:shareId/subjects

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 subjectId mayor que éste (el orden es estable por id). Usa el meta.nextCursor de la página anterior; es null cuando 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 removed los que salieron ({ subjectId, removedAt }). Pasa el meta.syncedAt de 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. Con since, totalCount cuenta 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

    full para la lectura ampliada con RUT completo.

  • Nombre
    include
    Tipo
    string
    Descripción

    nationalIdHash: cada titular trae nationalIdPartnerHash en 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.

Request
GET
/v1/.../shares/shr_1a2b3c/subjects
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
Response
{
  "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"
  }
}

GET/v1/organizations/:organizationId/shares/:shareId/consents

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, revoked o expired.

  • 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.nextCursor de la página anterior. Firmado contra la share y los filtros: reutilizarlo con otros responde 422 INVALID_CURSOR.

Request
GET
/v1/.../shares/shr_1a2b3c/consents
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
Response
{
  "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" }
}

POST/v1/organizations/:organizationId/shared-campaigns

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"]. whatsapp sale 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 responde 409 WHATSAPP_CHANNEL_UNAVAILABLE con details.reason (not_licensed, not_connected, setup_pending, setup_failed, disconnected, account_restricted, not_funded, templates_pending o spend_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) o per_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

    auto o bridge.

  • Nombre
    redirects
    Tipo
    array
    Descripción

    Hasta 2500 pares decididos al crear. Cada par nombra al estudiante por uno de rut o subjectId (el id de la lectura del grupo) y fija sólo lo que trae: successRedirectUrl, declineRedirectUrl, redirectMode, studentRedirectUrl (el enlace del aviso al estudiante) y partnerExternalId (tu correlativo, que luego viaja en webhooks, en eg_token y 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? }. Con enabled: 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 el studentRedirectUrl del sobre, o urlTemplate con {envelopeId} y {partnerExternalId} reemplazados. Sin correo del estudiante no se envía nada y la decisión no se ve afectada. En ambos casos recibes consent.student_notified con context.outcome (sent o no_contact). El apoderado sigue aterrizando en successRedirectUrl.

  • 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.

Request
POST
/v1/.../shared-campaigns
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" }]
  }'
Response
{
  "campaignId": "a9b8c7d6-e5f4-4321-a0b1-c2d3e4f5a6b7",
  "submittedShareCount": 1,
  "publicCompletionEnabled": true,
  "publicCompletionUrl": "https://app.edugoverna.com/consents/campaigns/…",
  "status": "accepted"
}

GET/v1/organizations/:organizationId/shared-campaigns/:campaignId

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ú) o registrar_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), inProgress y total.

  • Nombre
    coverage
    Tipo
    object
    Descripción

    Sólo en modo grupo: totalStudents, deliverableStudents, manualDeliveryStudents, authorityUndetermined, guardianMissing y, en modo sobre, awaitingSendStudents.

Request
GET
/v1/.../shared-campaigns/a9b8c7d6...
curl https://app.edugoverna.com/api/partner/v1/organizations/org_milectura/shared-campaigns/a9b8c7d6-e5f4-4321-a0b1-c2d3e4f5a6b7 \
  -H "Authorization: Bearer {keyId}.{secret}"
Response
{
  "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
  }
}

PATCH/v1/organizations/:organizationId/shared-campaigns/:campaignId

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.

Request
PATCH
/v1/.../shared-campaigns/a9b8c7d6...
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" }'
Response
{
  "campaignId": "a9b8c7d6-e5f4-4321-a0b1-c2d3e4f5a6b7",
  "status": "published",
  "endsAt": "2026-11-15T03:00:00.000Z",
  "origin": "operator"
}

POST/v1/organizations/:organizationId/shared-campaigns/:campaignId/close

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 emite consent.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 closed y 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.

Request
POST
/v1/.../shared-campaigns/a9b8c7d6.../close
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" }'
Response
{
  "campaignId": "a9b8c7d6-e5f4-4321-a0b1-c2d3e4f5a6b7",
  "status": "closed",
  "closedAt": "2026-10-02T14:03:22.511Z",
  "origin": "operator"
}

GET/v1/organizations/:organizationId/shared-campaigns/:campaignId/envelopes

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 el meta.syncedAt de una lectura como since de 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 limit se devuelve todo el roster, como siempre.

  • Nombre
    cursor
    Tipo
    string
    Descripción

    Paginación por llave (orden estable por envelopeId): pasa el meta.nextCursor de la página anterior; es null cuando no hay más.

Request
GET
/v1/.../shared-campaigns/a9b8c7d6.../envelopes
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
Response
{
  "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
    }
  ]
}

POST/v1/organizations/:organizationId/shared-campaigns/:campaignId/envelopes

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 (webhook consent.requested en la organización del titular). Trae además portalAccessLinks: 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, y consentRequestId es el de ese consentimiento.
  • not_in_campaign — el titular no está en el roster. En un envío individual esto es 404 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: errorCode dice por qué (CONSENT_AUTHORITY_UNDETERMINED si falta la fecha de nacimiento, GUARDIAN_AUTHORITY_REQUIRED si no hay apoderado vigente, SUBJECT_PROCESSING_OPPOSED si 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 un 409 con 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

    auto o bridge.

  • Nombre
    partnerExternalId
    Tipo
    string
    Descripción

    Tu correlativo para este sobre (1–160). Queda en el sobre y viaja en webhooks, en eg_token y 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 studentNotification activo. Prevalece sobre urlTemplate.

  • Nombre
    idempotencyKey
    Tipo
    string
    Descripción

    Llave de idempotencia (8–120).

Request
POST
/v1/.../shared-campaigns/a9b8c7d6.../envelopes
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"
  }'
Response
{
  "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.…"
    }
  ]
}

POST/v1/organizations/:organizationId/shared-campaigns/:campaignId/envelopes/:envelopeId/resend

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

    auto o bridge.

Request
POST
/v1/.../envelopes/env_7f8e9d/resend
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" }'
Response
{
  "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.…"
    }
  ]
}

POST/v1/organizations/:organizationId/shared-campaigns/:campaignId/envelope-jobs

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).

GET …/envelope-jobs/:jobId
{
  "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 }
}

GET/v1/organizations/:organizationId/shared-campaigns/:campaignId/envelopes/:envelopeId/student-contact

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.

Response
{
  "envelopeId": "886313e1-3b8a-5372-9b90-0c9aee199e5d",
  "email": "estudiante@colegio.cl"
}

GET/v1/organizations/:organizationId/shared-campaigns/:campaignId/envelopes/:envelopeId/evidence

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).

Response
{
  "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"
    }
  ]
}

POST/v1/organizations/:organizationId/shared-campaigns/:campaignId/sync-audience

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.

Request
POST
/v1/.../shared-campaigns/a9b8c7d6.../sync-audience
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" }
    ]
  }'
Response
{
  "addedContactCount": 2,
  "materializedRequestCount": 0,
  "pendingRequestCount": 0,
  "submittedRedirectCount": 1
}

POST/v1/organizations/:organizationId/shared-campaigns/:campaignId/redirects

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? }.

Request
POST
/v1/.../shared-campaigns/a9b8c7d6.../redirects
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" }
    ]
  }'
Response
{
  "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:

  1. Lee el kid del encabezado (typ: "eg-redirect+jwt") y toma esa clave de GET https://app.edugoverna.com/api/public/jwks.json. Puedes cachear el JWKS; si aparece un kid desconocido, vuelve a pedirlo.
  2. Verifica la firma y exige iss = "edugoverna", aud = tu partnerAccountId y exp en el futuro (vive 10 minutos; tolera un par de minutos de desfase de reloj).
  3. Usa las claims: consentRequestId, envelopeId, partnerExternalId, decision, decisionAuthority, decisionActorType, decidedAt y jti (ú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.

Verificación con jose
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, …

¿Te sirvió esta página?