---
title: "Consentimientos · API de organización"
description: "Campañas de consentimiento, solicitudes individuales y evidencia por API: ciclo de vida borrador → publicada → cerrada, canales, expiración y recaptura."
url: "https://docs.edugoverna.com/api-organizacion/consentimientos"
---
# Consentimientos

El motor de consentimiento gira en torno a dos recursos: la **campaña** (`consent_campaign`), que define a quién se le pide qué y por qué canal, y la **solicitud** (`consent_request`), el pedido individual a un titular o su apoderado, con sus entregas, su decisión y su evidencia sellada. Por API puedes crear y operar campañas, seguir cada solicitud y recuperar la evidencia; el registro manual de decisiones con documentos queda en la consola. {{ className: 'lead' }}

El recorrido completo —desde el diseño de la campaña hasta la decisión del apoderado— está narrado en la [guía del flujo de consentimiento](/guias/flujo-consentimiento); el trabajo entre organizaciones que comparten padrón, en [campañas compartidas](/guias/campanas-compartidas) y su [página de producto](/campanas-compartidas).

## El ciclo de vida de una campaña

<Properties>
  <Property name="status" type="string">
    `draft` → `published` → `closed`. Publicar exige que la audiencia esté
    materializada (`409 CONSENT_CAMPAIGN_AUDIENCE_NOT_READY` si no lo está);
    cerrar invalida las solicitudes pendientes y cancela los contactos no
    resueltos.
  </Property>
  <Property name="channels" type="string[]">
    `email` y/o `whatsapp`. `whatsapp` requiere que la organización tenga su
    número conectado en Ajustes → WhatsApp y los mensajes aprobados por Meta; al
    crear o publicar sin eso la API responde `409 WHATSAPP_CHANNEL_UNAVAILABLE`,
    con `details.reason` en `not_licensed`, `not_connected`, `setup_pending`, `setup_failed`, `disconnected`, `account_restricted`, `not_funded`, `templates_pending` o `spend_limit_reached`. No hay canal impreso: los formularios en papel son
    un respaldo manual (`manualFallback`) con su propio PDF en `GET
    /consent-campaigns/:id/print-forms`.
  </Property>
  <Property name="endsAt" type="timestamp | null">
    Expiración de la campaña; debe estar en el futuro al crearla. Al vencer, las
    solicitudes sin decisión pasan a `expired`.
  </Property>
  <Property name="campaignMode" type="string">
    `targeted` (audiencia definida por el colegio) o `public` (registro abierto
    con revisión posterior del personal).
  </Property>
  <Property name="dispatchMode" type="string">
    `bulk` (despacho masivo al publicar) o `per_subject` (sobres despachados uno
    a uno).
  </Property>
  <Property name="redirectMode" type="string">
    `auto` o `bridge`; junto con `defaultSuccessRedirectUrl` y
    `defaultDeclineRedirectUrl` controla a dónde vuelve el apoderado. Los
    destinos deben pertenecer a un [origen de redirección
    autorizado](/api-organizacion/derechos#origenes-de-redireccion).
  </Property>
</Properties>

---

## Listar campañas {{ tag: 'GET', label: '/api/consent-campaigns' }}

<Row>
  <Col>

    Requiere `consents.read`. Cada campaña llega con sus estadísticas (`stats`) calculadas: el embudo completo desde `pending` hasta `granted`/`denied`, tasa de completitud y desglose por canal. Pagina con `limit` y `cursor`.

  </Col>
  <Col sticky>

    <CodeGroup title="Solicitud" tag="GET" label="/api/consent-campaigns">

    ```bash {{ title: 'cURL' }}
    curl -G "https://app.edugoverna.com/api/consent-campaigns" \
      -H "x-api-key: {tu_api_key}" \
      -d limit=25
    ```

    ```js
    const { consentCampaigns, meta } = await fetch(
      'https://app.edugoverna.com/api/consent-campaigns?limit=25',
      { headers: { 'x-api-key': process.env.EDUGOVERNA_API_KEY } },
    ).then((r) => r.json())
    ```

    </CodeGroup>

    ```json {{ title: 'Respuesta (recortada)' }}
    {
      "consentCampaigns": [
        {
          "id": "c81d…",
          "name": "Salida pedagógica agosto",
          "status": "published",
          "campaignMode": "targeted",
          "dispatchMode": "bulk",
          "channels": ["email", "whatsapp"],
          "endsAt": "2026-09-10T03:59:59.000Z",
          "publishedAt": "2026-08-20T13:00:00.000Z",
          "closedAt": null,
          "publicUrl": null,
          "processingActivity": {
            "processingActivityId": "a2f1…",
            "activityCode": "ACT-004",
            "activityName": "Comunicaciones académicas",
            "versionTitle": "v2 · salidas pedagógicas",
            "versionNumber": 2
          },
          "stats": {
            "total": 182,
            "pending": 4,
            "sent": 170,
            "opened": 121,
            "completed": 96,
            "granted": 88,
            "denied": 8,
            "revoked": 0,
            "expired": 0,
            "failed": 2,
            "errored": 2,
            "completionRate": 0.53,
            "channelBreakdown": { "email": 150, "whatsapp": 32 }
          }
          // …
        }
      ],
      "meta": {
        "totalMatchedCount": 12,
        "limit": 25,
        "returnedCount": 12,
        "nextCursor": null,
        "hasMore": false
      }
    }
    ```

  </Col>
</Row>

---

## Crear una campaña {{ tag: 'POST', label: '/api/consent-campaigns' }}

<Row>
  <Col>

    Requiere `consents.write`. Si defines la audiencia por Generaciones, Cursos o estudiantes individuales, la llamada exige además `students.read`; si usas padrones compartidos (`sharedAudience`), `shared_campaigns.write`. La campaña nace en `draft`; publicarla es un paso aparte.

    ### Atributos requeridos

    <Properties>
      <Property name="processingActivityVersionId" type="string">
        La versión de la actividad de tratamiento cuyo consentimiento se pide.
        Si esa versión es de modelo granular, las opciones de consentimiento
        vienen de ella — la campaña no las define.
      </Property>
      <Property name="name" type="string">
        Nombre de la campaña (2 a 160 caracteres).
      </Property>
    </Properties>

    ### Atributos opcionales frecuentes

    <Properties>
      <Property name="channels" type="string[]">
        `["email"]` por defecto; hasta 2 (`email`, `whatsapp`). `whatsapp`
        exige el canal conectado de la organización; si no,
        `409 WHATSAPP_CHANNEL_UNAVAILABLE` (`details.reason`).
      </Property>
      <Property name="endsAt" type="timestamp">
        Expiración, en el futuro.
      </Property>
      <Property name="audience" type="object">
        `{ cohortIds, segmentIds, studentSubjectIds }` — Generaciones, Cursos
        o estudiantes puntuales (hasta 1000).
      </Property>
      <Property name="reminderDayOffsets" type="integer[]">
        Hasta 3 recordatorios, en días crecientes (1 a 30). `[]` los apaga.
      </Property>
      <Property name="dispatchMode" type="string">
        `bulk` o `per_subject`.
      </Property>
      <Property name="sharedAudience" type="object">
        `{ shareIds }` para campañas sobre padrones compartidos.
      </Property>
      <Property name="id" type="string">
        UUID generado por el cliente: hace idempotente el envío repetido.
      </Property>
    </Properties>

  </Col>
  <Col sticky>

    <CodeGroup title="Solicitud" tag="POST" label="/api/consent-campaigns">

    ```bash {{ title: 'cURL' }}
    curl -X POST "https://app.edugoverna.com/api/consent-campaigns" \
      -H "x-api-key: {tu_api_key}" \
      -H "Content-Type: application/json" \
      -d '{
        "processingActivityVersionId": "9d4e…",
        "name": "Salida pedagógica agosto",
        "channels": ["email", "whatsapp"],
        "endsAt": "2026-09-10T03:59:59.000Z",
        "audience": { "segmentIds": ["5b2c…"] }
      }'
    ```

    ```js
    const { consentCampaign } = await fetch(
      'https://app.edugoverna.com/api/consent-campaigns',
      {
        method: 'POST',
        headers: {
          'x-api-key': process.env.EDUGOVERNA_API_KEY,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          processingActivityVersionId: '9d4e…',
          name: 'Salida pedagógica agosto',
          channels: ['email', 'whatsapp'],
          endsAt: '2026-09-10T03:59:59.000Z',
          audience: { segmentIds: ['5b2c…'] },
        }),
      },
    ).then((r) => r.json())
    ```

    </CodeGroup>

    ```json {{ title: 'Respuesta · 201 (recortada)' }}
    {
      "consentCampaign": {
        "id": "c81d…",
        "name": "Salida pedagógica agosto",
        "status": "draft",
        "channels": ["email", "whatsapp"],
        "endsAt": "2026-09-10T03:59:59.000Z",
        "stats": { "total": 0, "pending": 0, "completed": 0 }
        // …
      }
    }
    ```

  </Col>
</Row>

---

## Detalle y operación de la campaña

`GET /consent-campaigns/:campaignId` (con `consents.read`) devuelve `consentCampaign`, `processingActivity`, los `contacts` paginados (cada uno con su solicitud, entregas y última decisión), `outcomes` (decisiones agrupadas), `coverage` (cuántos estudiantes son alcanzables y por qué no los demás) y `pendingVerificationCount` para el modo público. El resto de la operación:

| Método y ruta                                            | Permiso          | Qué hace                                                                             |
| -------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------ |
| `POST /consent-campaigns/:id/publish`                    | `consents.write` | Publica: `draft` → `published` y despacha según `dispatchMode`.                      |
| `PATCH /consent-campaigns/:id`                           | `consents.write` | Edita la campaña (también publicada; los canales, sólo en borrador).                |
| `GET /consent-campaigns/:id/coverage`                    | `consents.read`  | Cobertura de audiencia sin abrir el detalle.                                         |
| `POST /consent-campaigns/audience-coverage`              | `consents.read`  | Simula la cobertura de una audiencia antes de crear.                                 |
| `POST /consent-campaigns/:id/resend-pending`             | `consents.write` | Reenvía a todos los pendientes.                                                      |
| `POST /consent-campaigns/:id/contacts/:contactId/resend` | `consents.write` | Reenvía a un contacto puntual.                                                       |
| `POST /consent-campaigns/:id/process-error-contacts`     | `consents.write` | Reprocesa contactos con datos de contacto erróneos.                                  |
| `POST /consent-campaigns/:id/contacts/:contactId/verify` | `consents.write` | Aprueba un registro público pendiente de revisión.                                   |
| `POST /consent-campaigns/:id/contacts/:contactId/reject` | `consents.write` | Rechaza un registro público (cascada sobre lo acuñado).                              |
| `POST /consent-campaigns/:id/envelopes`                  | `consents.write` | Despacha sobres (modo `per_subject` / compartidas).                                  |
| `POST /consent-campaigns/:id/sync-audience`              | `consents.write` | Resincroniza la audiencia con el padrón compartido.                                  |
| `POST /consent-campaigns/:id/redirects`                  | `consents.write` | Carga redirects explícitos por RUT.                                                  |
| `POST /consent-campaigns/redirect-pairs/parse`           | `consents.write` | Parsea un archivo de pares RUT → URL antes de cargarlo.                              |
| `GET /consent-campaigns/:id/print-forms`                 | `consents.read`  | PDF con los formularios impresos del respaldo manual.                                |
| `POST /consent-campaigns/:id/close`                      | `consents.write` | Cierra: anula las pendientes (cada una emite `consent.invalidated`) y responde los contadores `invalidatedRequestCount` y `cancelledContactCount`. |

---

## Listar solicitudes de consentimiento {{ tag: 'GET', label: '/api/consents/requests' }}

<Row>
  <Col>

    Requiere `consents.read`. Cada elemento es la solicitud completa: estado, entregas por canal, última decisión con sus opciones y evidencia referenciada, y el resumen del titular bajo la proyección de contacto que tu key permita (`subjectContactProjection`: `none`, `masked` o `full`).

    ### Parámetros opcionales

    <Properties>
      <Property name="scope" type="string">
        `activas`, `por-expirar`, `otorgadas`, `denegadas`, `revocadas`,
        `expiradas` o `recaptura`.
      </Property>
      <Property name="subjectId" type="string">
        Todas las solicitudes de un titular.
      </Property>
      <Property name="q" type="string">
        Búsqueda (hasta 120 caracteres).
      </Property>
      <Property name="counts" type="string">
        `counts=1` agrega `meta.scopeCounts` con los contadores por alcance.
      </Property>
      <Property name="sort / dir / limit / cursor" type="varios">
        `sort`: `solicitada`, `estado`, `vence`. `limit` por defecto 100,
        máximo 250.
      </Property>
    </Properties>

  </Col>
  <Col sticky>

    <CodeGroup title="Solicitud" tag="GET" label="/api/consents/requests">

    ```bash {{ title: 'cURL' }}
    curl -G "https://app.edugoverna.com/api/consents/requests" \
      -H "x-api-key: {tu_api_key}" \
      -d scope=otorgadas -d counts=1
    ```

    ```js
    const { consentRequests, meta } = await fetch(
      'https://app.edugoverna.com/api/consents/requests?scope=otorgadas&counts=1',
      { headers: { 'x-api-key': process.env.EDUGOVERNA_API_KEY } },
    ).then((r) => r.json())
    ```

    </CodeGroup>

    ```json {{ title: 'Respuesta (recortada)' }}
    {
      "consentRequests": [
        {
          "id": "b9d2…",
          "status": "granted",
          "requestedAt": "2026-08-20T13:05:00.000Z",
          "expiresAt": "2026-09-10T03:59:59.000Z",
          "consentCampaignId": "c81d…",
          "requestRole": "consent",
          "guardianAuthorization": null,
          "processingActivityTitle": "Comunicaciones académicas",
          "subjectContactProjection": "masked",
          "targetSubjectSummary": {
            "id": "3f6f…",
            "subjectType": "student",
            "referenceCode": "EST-2026-0412",
            "currentAgeBand": "minor_14_17",
            "displayName": "Martina Rojas",
            "primaryContact": {
              "contactType": "email",
              "value": null,
              "maskedValue": "m***@example.cl"
            }
          },
          "deliveries": [
            { "channel": "email", "status": "opened", "sentAt": "2026-08-20T13:06:00.000Z" }
          ],
          "latestDecision": {
            "decision": "granted",
            "options": null,
            "evidence": [
              { "evidenceType": "sealed_manifest", "checksum": "e4a6acc35331fc53" }
            ]
          }
          // …
        }
      ],
      "meta": {
        "totalMatchedCount": 96,
        "limit": 100,
        "returnedCount": 96,
        "hasMore": false,
        "nextCursor": null
      }
    }
    ```

  </Col>
</Row>

### Autorización del apoderado

Si la versión de la actividad tiene `requiresGuardianAuthorization` (salidas, giras) y el titular decide por sí mismo según su edad, junto a su solicitud se abre otra para que el apoderado **autorice la actividad**. La lista trae sólo las solicitudes de los titulares; cada una lleva `requestRole: "consent"` y, en `guardianAuthorization`, el estado de la autorización del apoderado (`{ requestId, status, decidedAt }`, o `null` si no corresponde). La del apoderado se pide por su `requestId`: tiene `requestRole: "guardian_authorization"` y, en `companionOfRequestId`, el `id` de la solicitud del titular. No emite [webhooks](/webhooks).

El detalle (`GET /consents/requests/:consentRequestId`) devuelve el mismo cuerpo aplanado más `consentModel` (`bundled` o `granular`) y `versionConsentOptions` — las opciones granulares definidas por la versión de la actividad. `GET /consents/requests/:id/print-form` entrega el formulario individual en PDF.

---

## Evidencia de consentimiento {{ tag: 'GET', label: '/api/consents/evidence' }}

<Row>
  <Col>

    Requiere `consents.read`. Una fila por artefacto de evidencia: manifiestos sellados, certificados de recibo y documentos de firma manual, cada uno con su `checksum`. La búsqueda `q` cruza checksum, ids y actividad — nunca datos de personas.

    ### Parámetros opcionales

    <Properties>
      <Property name="tipo" type="string">
        Filtra por `evidenceType` (p. ej. `sealed_manifest`,
        `receipt_certificate`, `receipt_certificate_pdf`,
        `manual_signature_document`).
      </Property>
      <Property name="decision" type="string">
        `granted` o `denied`.
      </Property>
      <Property name="limit / offset" type="integer">
        Por defecto 25, máximo 100.
      </Property>
    </Properties>

    <Note>
      La descarga de los bytes
      (`GET /consents/evidence/:artifactId/download`) es **sólo disponible con
      sesión de la consola** (permiso `consent_evidence.download`): la API
      entrega el índice y los checksums, no el contenido.
    </Note>

  </Col>
  <Col sticky>

    <CodeGroup title="Solicitud" tag="GET" label="/api/consents/evidence">

    ```bash {{ title: 'cURL' }}
    curl -G "https://app.edugoverna.com/api/consents/evidence" \
      -H "x-api-key: {tu_api_key}" \
      -d tipo=sealed_manifest
    ```

    ```js
    const { consentEvidence, meta } = await fetch(
      'https://app.edugoverna.com/api/consents/evidence?tipo=sealed_manifest',
      { headers: { 'x-api-key': process.env.EDUGOVERNA_API_KEY } },
    ).then((r) => r.json())
    ```

    </CodeGroup>

    ```json {{ title: 'Respuesta (recortada)' }}
    {
      "consentEvidence": [
        {
          "id": "ev01…",
          "evidenceType": "sealed_manifest",
          "checksum": "e4a6acc35331fc53",
          "capturedAt": "2026-08-21T02:11:09.000Z",
          "artifactId": "ar77…",
          "decision": "granted",
          "consentRequestId": "b9d2…",
          "requestStatus": "granted",
          "processingActivityTitle": "Comunicaciones académicas",
          "targetSubjectSummary": { "referenceCode": "EST-901" },
          "sourceOrganization": null
        }
      ],
      "meta": {
        "totalMatchedCount": 3,
        "limit": 25,
        "offset": 0,
        "returnedCount": 3,
        "typeCounts": { "sealed_manifest": 1, "receipt_certificate": 1, "manual_signature_document": 1 },
        "totalCount": 3
      }
    }
    ```

  </Col>
</Row>

---

## Solicitudes sueltas y recaptura

| Método y ruta                                         | Permiso          | Qué hace                                                                                                                                                                                                                                                        |
| ----------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /consents/requests`                             | `consents.write` | Crea una solicitud individual fuera de campaña.                                                                                                                                                                                                                 |
| `POST /consents/recapture`                            | `consents.write` | Al publicar una versión nueva de la actividad, acuña solicitudes de recaptura: `processingActivityVersionId` requerido, `targetSubjectIds` opcional (omitido = todos los titulares con consentimiento de versiones anteriores). Responde `{ consentRequests }`. |
| `POST /consents/deliveries/:consentDeliveryId/status` | `consents.write` | Actualiza el estado de una entrega (`queued`, `sent`, `delivered`, `opened`, `failed`).                                                                                                                                                                         |

<Note>
  **Sólo disponible con sesión de la consola:** el registro manual de una
  decisión (`POST /consents/requests/:id/decisions`, permiso
  `consents.record.manual`), la subida de evidencia firmada (`POST
  /consents/requests/:id/evidence-uploads`) y la revocación administrativa
  (`POST /consents/requests/:id/revoke`). Registrar a mano lo que un apoderado
  decidió exige una persona identificable detrás.
</Note>
