---
title: "Flujo de consentimiento de principio a fin"
description: "Cómo crear una solicitud de consentimiento por la API de socios, qué recibe la familia y cómo seguir la decisión por polling o webhooks."
url: "https://docs.edugoverna.com/guias/flujo-consentimiento"
---
# Flujo de consentimiento de principio a fin

En esta guía recorrerás el ciclo de vida completo de una solicitud de consentimiento: desde la llamada que la crea, pasando por lo que ve la familia en su correo o WhatsApp, hasta el momento en que tu plataforma recibe la decisión y puede actuar sobre ella. {{ className: 'lead' }}

El flujo, de principio a fin, se ve así:

1. Tu plataforma crea la solicitud con `POST /consents/requests` → respuesta `201` y evento `consent.requested`.
2. Edugoverna encola la entrega por email o WhatsApp según el contacto primario del titular → eventos `consent.delivery.updated` por cada transición de estado (`sent`, `delivered`, `failed`, …) y `consent.delivery.failed` si el envío rebota.
3. El apoderado o el titular abre el enlace y llega al portal público de consentimiento → evento `consent.delivery.opened`.
4. Lee el aviso en lenguaje claro, verifica su identidad y decide → evento `consent.decision.recorded`; el portal lo redirige a tu plataforma según `redirectMode`.
5. Si más adelante el titular revoca, la solicitud vence sin respuesta o se cierra su campaña → eventos `consent.revoked`, `consent.expired` o `consent.invalidated`.

## 1. Prerrequisitos

Antes de crear solicitudes de consentimiento necesitas tres cosas del colegio con el que vas a trabajar:

- **Una conexión activa.** El colegio (la organización dueña de los datos) debe haber aceptado la conexión con tu cuenta de socio. Revisa el estado en [Gestión » Conexiones](/gestion/conexiones).
- **Un encargo de tratamiento vigente (DPA).** La conexión formaliza tu rol como encargado según el Art. 15 bis de la Ley 21.719; sin ese acuerdo la conexión no se activa.
- **Los permisos correctos en tu credencial.** Crear solicitudes requiere `partner.consents.write`; listarlas y consultarlas requiere `partner.consents.read`. Las credenciales se administran en [Gestión » Credenciales](/gestion/credenciales).

Todas las llamadas de esta guía se autentican con tu llave de socio y se dirigen a la organización del colegio:

```bash
https://app.edugoverna.com/api/partner/v1/organizations/:organizationId
Authorization: Bearer {keyId}.{secret}
```

El detalle del esquema de autenticación está en [Autenticación](/autenticacion).

<Note>
  Si vas a usar `successRedirectUrl` o `declineRedirectUrl`, el origen de esas
  URLs (esquema + host + puerto) debe estar registrado previamente en la lista
  de orígenes de redirección de la conexión. Una URL con origen no registrado
  hace fallar la creación de la solicitud completa.
</Note>

## 2. Sincroniza y verifica el padrón

Una solicitud de consentimiento apunta a un titular del padrón del colegio por su `referenceCode` — el código de referencia estable que identifica al estudiante sin exponer su RUT. Antes de emitir, confirma que el titular existe y tiene un canal de contacto utilizable. La estructura del padrón y cómo mantenerlo sincronizado se describen en [Padrón](/padron) y en la referencia de [Sujetos](/api-organizacion/sujetos).

Si el titular no tiene ningún contacto registrado, la solicitud se crea igualmente, pero su entrega queda como registro manual — canal `manual`, estado `pending_manual` — y nadie la despacha automáticamente. El token de esa entrega sí viaja en `deliveryAccessLinks`, así que puedes hacer llegar el enlace del portal por tus propios medios, o esperar a que exista un canal. Emitir contra un padrón verificado te ahorra ese embudo.

## 3. Crea la solicitud de consentimiento

Identifica la actividad de tratamiento por su versión exacta (`processingActivityVersionId`) o por su código (`processingActivityCode`, opcionalmente con `processingActivityVersionNumber`); uno de los dos es obligatorio. Las actividades de tratamiento y sus versiones se administran según [Actividades de tratamiento](/actividades-tratamiento).

<CodeGroup tag="POST" label="/consents/requests">

```bash {{ title: 'cURL' }}
curl https://app.edugoverna.com/api/partner/v1/organizations/{organizationId}/consents/requests \
  -H "Authorization: Bearer {keyId}.{secret}" \
  -H "Content-Type: application/json" \
  -d '{
    "targetSubjectReferenceCode": "EST-2026-0142",
    "processingActivityCode": "fotografias-actividades",
    "successRedirectUrl": "https://plataforma.ejemplo.cl/consentimientos/gracias",
    "declineRedirectUrl": "https://plataforma.ejemplo.cl/consentimientos/rechazado",
    "redirectMode": "auto",
    "partnerExternalId": "alumno-8841",
    "idempotencyKey": "fotos-2026-EST-2026-0142"
  }'
```

```js
const response = await fetch(
  `https://app.edugoverna.com/api/partner/v1/organizations/${organizationId}/consents/requests`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${keyId}.${secret}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      targetSubjectReferenceCode: "EST-2026-0142",
      processingActivityCode: "fotografias-actividades",
      successRedirectUrl:
        "https://plataforma.ejemplo.cl/consentimientos/gracias",
      declineRedirectUrl:
        "https://plataforma.ejemplo.cl/consentimientos/rechazado",
      redirectMode: "auto",
      partnerExternalId: "alumno-8841",
      idempotencyKey: "fotos-2026-EST-2026-0142",
    }),
  }
)

const consentRequest = await response.json()
```

</CodeGroup>

Los campos del cuerpo:

- `targetSubjectReferenceCode` — obligatorio. El código de referencia del estudiante en el padrón del colegio.
- `processingActivityVersionId` **o** `processingActivityCode` — obligatorio uno de los dos. Con código, puedes fijar una versión con `processingActivityVersionNumber`; si no, se usa la vigente.
- `successRedirectUrl` / `declineRedirectUrl` — opcionales. Adónde vuelve el titular tras otorgar o rechazar. Sus orígenes deben estar en la lista blanca de la conexión.
- `redirectMode` — opcional, `"auto"` (por defecto) o `"bridge"`. Con `auto` el portal redirige solo tras confirmar la decisión; con `bridge` muestra una página intermedia con un botón «Continuar» que el titular pulsa a su ritmo.
- `partnerExternalId` — opcional. Tu propio identificador del caso (1–160 caracteres); luego puedes filtrar el listado por él.
- `idempotencyKey` — opcional pero muy recomendado (8–120 caracteres). Repetir la llamada con la misma llave devuelve la misma solicitud en lugar de crear otra.

La respuesta `201` contiene la solicitud con sus entregas ya planificadas. Entre otros campos:

```json {{ title: 'Respuesta (fragmento)' }}
{
  "id": "0d9a2c1e-6d61-4f1c-9f5a-3f2f8b6f1a77",
  "status": "pending",
  "targetSubjectId": "8f3b7f0a-2f9d-4d4e-a1c3-77b2a0f1d942",
  "processingActivityVersionId": "3f7e6c9d-8f21-4b83-bb1c-5e2d9a447f10",
  "partnerExternalId": "alumno-8841",
  "redirectMode": "auto",
  "requestedAt": "2026-08-25T13:40:12.000Z",
  "expiresAt": null,
  "deliveries": [
    {
      "id": "5b1d0c8e-90aa-4a6f-8f0d-df31c2b6a3e4",
      "channel": "email",
      "destinationMasked": "m•••a@correo.cl",
      "status": "queued"
    }
  ],
  "deliveryAccessLinks": [
    {
      "consentDeliveryId": "5b1d0c8e-90aa-4a6f-8f0d-df31c2b6a3e4",
      "recipientSubjectId": "c2a4e8d1-13b7-4f6e-9a20-4e8b7c1d5f30",
      "channel": "email",
      "token": "5b1d0c8e-90aa-4a6f-8f0d-df31c2b6a3e4.J8kQ…"
    }
  ],
  "latestDecision": null,
  "idempotentReplay": false
}
```

<Note>
  Una repetición idempotente (`idempotentReplay: true`) devuelve la solicitud
  original **sin** tokens frescos: `deliveryAccessLinks` llega vacío. Los tokens
  de acceso al portal solo se emiten en la creación.
</Note>

## 4. Qué recibe la familia

Edugoverna despacha la entrega por el canal registrado del destinatario — email o WhatsApp — con recordatorios automáticos si la solicitud lo contempla. El envío por WhatsApp sale desde el número que la organización conectó en Ajustes → WhatsApp, con mensajes aprobados por Meta; si el canal no está listo, la solicitud con `whatsapp` se rechaza con `409 WHATSAPP_CHANNEL_UNAVAILABLE` (`details.reason`). El mensaje lleva un enlace único al portal público de consentimiento:

```text {{ title: 'Enlace del portal' }}
https://app.edugoverna.com/consents/portal/{token}
```

El `token` es el mismo que recibes en `deliveryAccessLinks`, así que también puedes mostrar el enlace dentro de tu propia plataforma (por ejemplo, en el perfil del apoderado) sin esperar el correo. Trátalo como un secreto: quien tenga el enlace puede ver el aviso y decidir.

En el portal, el apoderado o el titular:

1. Ve el aviso en lenguaje claro de la actividad de tratamiento — finalidades, categorías de datos, destinatarios, plazos de conservación — con la marca del colegio y, si corresponde, la tuya.
2. Verifica su identidad según el modo configurado para la solicitud.
3. Otorga o rechaza. Si el consentimiento es granular, decide permiso por permiso.
4. Es redirigido a tu `successRedirectUrl` o `declineRedirectUrl` según su decisión: tras una breve confirmación con `redirectMode: "auto"`, o mediante un botón de continuar con `"bridge"`. La URL de destino llega con `consent_id` y `decision` como parámetros de consulta, para que tu plataforma retome el caso sin adivinar.

Cada decisión queda registrada con su evidencia (aviso sellado con checksum, momento, método de captura) del lado del colegio; tu plataforma no necesita almacenar nada de eso para acreditar el consentimiento.

## 5. Sigue el estado: polling o webhooks

### Polling con `since`

Para integraciones simples basta con consultar periódicamente el listado, filtrando por fecha:

<CodeGroup tag="GET" label="/consents/requests">

```bash {{ title: 'cURL' }}
curl -G https://app.edugoverna.com/api/partner/v1/organizations/{organizationId}/consents/requests \
  -H "Authorization: Bearer {keyId}.{secret}" \
  -d since=2026-08-25T00:00:00Z \
  -d limit=100
```

```js
const params = new URLSearchParams({
  since: "2026-08-25T00:00:00Z",
  limit: "100",
})

const response = await fetch(
  `https://app.edugoverna.com/api/partner/v1/organizations/${organizationId}/consents/requests?${params}`,
  { headers: { Authorization: `Bearer ${keyId}.${secret}` } }
)

const { consentRequests, nextCursor } = await response.json()
```

</CodeGroup>

Filtros disponibles: `status`, `processingActivityCode`, `partnerExternalId`, `since` (solicitudes creadas después de esa fecha) y paginación por `cursor` + `limit` (máximo 200 por página; 50 por defecto). El cursor está amarrado a la organización y a los filtros con que se emitió: reanudar el recorrido con filtros distintos responde `422 INVALID_CURSOR`. Más detalle en [Paginación](/paginacion).

El detalle de una solicitud puntual está en `GET /consents/requests/:consentRequestId`.

### Webhooks

Para reaccionar en tiempo real, suscribe un endpoint a la familia de eventos de consentimiento desde [Gestión » Webhooks](/gestion/webhooks). El mecanismo general — firma, reintentos, formato — está en [Webhooks](/webhooks).

<Row>
  <Col>

    <Properties>
      <Property name="consent.requested">
        Se creó una solicitud de consentimiento.
      </Property>
      <Property name="consent.delivery.updated">
        Una entrega cambió de estado (`sent`, `delivered`, `failed`, …). Cada
        transición llega como un despacho propio, con el estado nuevo en
        `data`.
      </Property>
      <Property name="consent.delivery.opened">
        El destinatario abrió el portal desde su enlace.
      </Property>
      <Property name="consent.delivery.failed">
        La entrega falló de forma definitiva (rebote, número inexistente).
      </Property>
      <Property name="consent.decision.recorded">
        Se registró una decisión: otorgada o rechazada.
      </Property>
      <Property name="consent.revoked">
        El titular revocó un consentimiento otorgado.
      </Property>
      <Property name="consent.expired">
        La solicitud venció sin decisión.
      </Property>
      <Property name="consent.invalidated">
        Se cerró la campaña de la solicitud antes de que hubiera decisión.
      </Property>
    </Properties>

  </Col>
  <Col sticky>

    ```json {{ title: 'Ejemplo de payload' }}
    {
      "event": "consent.decision.recorded",
      "sourceType": "consent_request",
      "sourceId": "0d9a2c1e-6d61-4f1c-9f5a-3f2f8b6f1a77",
      "occurredAt": "2026-08-25T14:03:22.000Z",
      "data": {
        "id": "0d9a2c1e-6d61-4f1c-9f5a-3f2f8b6f1a77",
        "status": "granted",
        "partnerExternalId": "alumno-8841",
        "latestDecision": {
          "decision": "granted",
          "decisionAuthority": "guardian",
          "captureMethod": "portal_link_otp",
          "decidedAt": "2026-08-25T14:03:21.000Z"
        }
      }
    }
    ```

  </Col>
</Row>

Cada despacho llega firmado: la cabecera `x-edugoverna-signature` (configurable por endpoint) trae el HMAC-SHA256 de `{timestamp}.{cuerpo}` con el secreto del endpoint, y `x-edugoverna-timestamp` el timestamp en segundos. `x-edugoverna-event` repite el tipo de evento.

## 6. Registra evidencia y buenas prácticas

- **Idempotencia al crear.** Usa siempre `idempotencyKey` con una llave estable derivada de tu propio dominio (por ejemplo `actividad + estudiante + campaña`). Un reintento de red o un doble clic devolverán la misma solicitud, nunca una segunda.
- **Idempotencia al consumir.** Los webhooks se entregan _al menos una vez_. Deduplica con `event` + `sourceId` + el contenido relevante de `data` (por ejemplo, el `status` en los eventos de entrega): dos despachos con esa misma combinación son el mismo hecho. Más detalle en [Webhooks](/webhooks#requisitos-y-garantias-de-entrega).
- **Responde rápido, procesa después.** Contesta `2xx` apenas verifiques la firma y encola el procesamiento. Un endpoint que responde error queda registrado como entrega fallida y el evento se reintenta.
- **Actúa sobre la decisión, no sobre la entrega.** Un `consent.delivery.updated` con estado `delivered` significa que el mensaje llegó, no que hay consentimiento. Habilita el tratamiento solo con `consent.decision.recorded` en `granted`, y detenlo con `consent.revoked`, `consent.invalidated` o `consent.recaptured`. `consent.expired` sólo le llega a una solicitud que nunca se decidió: el cierre de una campaña no vence un «sí».
- **Complementa con polling.** Aunque uses webhooks, un barrido periódico con `since` es la red de seguridad contra ventanas en que tu endpoint estuvo caído.

## ¿Qué sigue?

- [Consentimientos: conceptos y estados](/consentimientos)
- [Referencia del recurso de consentimientos](/api-organizacion/consentimientos)
- [Campañas sobre registros compartidos](/guias/campanas-compartidas), cuando el colegio te comparte cursos completos de su padrón
- [Errores y códigos de la API](/errores) y [límites de tasa](/limites)
