---
title: "Campañas sobre registros compartidos"
description: "Cómo descubrir los cursos que un colegio te compartió, crear una campaña de consentimiento sobre ellos, emitir sobres y mantener la audiencia sincronizada."
url: "https://docs.edugoverna.com/guias/campanas-compartidas"
---
# Campañas sobre registros compartidos

Un colegio puede compartir contigo cursos completos de su padrón — un _registry share_ — para que tu plataforma solicite consentimientos a esas familias sin que el colegio tenga que operar la campaña. En esta guía descubrirás los shares disponibles, crearás una campaña compartida, emitirás sobres uno a uno y mantendrás la audiencia al día cuando el colegio actualice su padrón. {{ className: 'lead' }}

El flujo completo:

1. El colegio comparte uno o más cursos con tu cuenta de socio (concede el _share_).
2. Tu plataforma lista los shares y, si lo necesita, sus integrantes → `GET /shares`, `GET /shares/:shareId/subjects`.
3. Creas y publicas la campaña sobre esos shares → `POST /shared-campaigns`, respuesta `202`.
4. En modo `bulk` Edugoverna despacha el roster completo; en modo `per_subject` emites cada sobre cuando tu producto lo decide → `POST .../envelopes`.
5. Cada familia recibe su entrega y decide en el portal → tu organización recibe el webhook `consent.decision.recorded` con la proyección `shared_partner`.
6. Cuando el colegio matricula estudiantes nuevos, sincronizas la audiencia → `POST .../sync-audience`.
7. Para conciliar el estado de todo el curso —de todas tus campañas sobre él, también las declaradas por un registrador— lees `GET /shares/:shareId/consents` con `updatedSince`.

## Qué es un registro compartido

Un share es una concesión explícita del colegio: «este curso de mi padrón es visible para este socio, para este fin». Lo que ves de cada integrante es una proyección acotada — nombre para mostrar, tramo etario, RUT enmascarado y _presencia_ de canales de contacto, nunca sus valores. Los datos siguen viviendo en la organización del colegio; la campaña, en cambio, vive en la tuya. El detalle conceptual está en [Campañas compartidas](/campanas-compartidas).

<Note>
  Atención con el `:organizationId` de cada ruta. Las rutas de descubrimiento
  (`/shares`, `/shares/:shareId/subjects`, `/shares/:shareId/consents`) usan el id de la **organización del
  colegio** dueña del padrón. Las rutas de campaña (`/shared-campaigns` y todo
  lo que cuelga de ellas) usan el id de **tu propia organización de trabajo**,
  porque la campaña vive ahí. Apuntar una campaña a la organización del colegio
  responde `403 PARTNER_WORKSPACE_REQUIRED`.
</Note>

## 1. Descubre los shares y sus integrantes

<CodeGroup tag="GET" label="/shares">

```bash {{ title: 'cURL' }}
curl https://app.edugoverna.com/api/partner/v1/organizations/{colegioOrgId}/shares \
  -H "Authorization: Bearer {keyId}.{secret}"
```

```js
const response = await fetch(
  `https://app.edugoverna.com/api/partner/v1/organizations/${colegioOrgId}/shares`,
  { headers: { Authorization: `Bearer ${keyId}.${secret}` } }
)

const { shares } = await response.json()
```

</CodeGroup>

```json {{ title: 'Respuesta' }}
{
  "shares": [
    {
      "id": "b1f6a3d2-4c1e-4e29-9d5b-8a0f7c3e2d11",
      "groupType": "segment",
      "groupKind": "course",
      "groupName": "3° Básico A 2026",
      "memberCount": 32,
      "grantSource": "school_dashboard",
      "createdAt": "2026-03-04T12:00:00.000Z"
    }
  ]
}
```

Los integrantes de un share se listan con paginación por `offset` y `limit`:

<CodeGroup tag="GET" label="/shares/:shareId/subjects">

```bash {{ title: 'cURL' }}
curl -G https://app.edugoverna.com/api/partner/v1/organizations/{colegioOrgId}/shares/{shareId}/subjects \
  -H "Authorization: Bearer {keyId}.{secret}" \
  -d offset=0 \
  -d limit=50
```

```js
const response = await fetch(
  `https://app.edugoverna.com/api/partner/v1/organizations/${colegioOrgId}/shares/${shareId}/subjects?offset=0&limit=50`,
  { headers: { Authorization: `Bearer ${keyId}.${secret}` } }
)

const { share, subjects, meta } = await response.json()
```

</CodeGroup>

```json {{ title: 'Respuesta' }}
{
  "share": {
    "id": "b1f6a3d2-4c1e-4e29-9d5b-8a0f7c3e2d11",
    "groupType": "segment",
    "groupKind": "course",
    "groupName": "3° Básico A 2026",
    "memberCount": 32
  },
  "subjects": [
    {
      "subjectId": "8f3b7f0a-2f9d-4d4e-a1c3-77b2a0f1d942",
      "displayName": "Martina P.",
      "subjectType": "student",
      "currentAgeBand": "under_14",
      "maskedRut": "••.•••.634-5",
      "contacts": [],
      "consentReach": {
        "general": { "decidedBy": "guardian", "status": "ok", "channels": ["email"] },
        "sensitive": { "decidedBy": "guardian", "status": "ok", "channels": ["email"] }
      }
    }
  ],
  "meta": {
    "totalCount": 32,
    "offset": 0,
    "limit": 50,
    "missingContactCount": 3,
    "unreachableCount": { "general": 3, "sensitive": 3 }
  }
}
```

`contacts` son los canales propios del estudiante; lo que importa para la campaña es `consentReach`, que dice quién decide por él (el estudiante o su apoderado, según edad y si tu finalidad trata datos sensibles) y por qué canales se le alcanza. `meta.missingContactCount` te anticipa cuántos integrantes del share tienen un decisor inalcanzable —sin contacto, sin apoderado con autoridad o sin fecha de nacimiento—: son los candidatos a la puerta pública de completación (más abajo). Si el colegio habilitó la lectura ampliada, puedes pedirla explícitamente con `?disclosure=full`; es un acto deliberado que queda auditado por sí mismo.

## 2. Crea la campaña compartida

Una sola llamada crea **y publica** la campaña en tu organización de trabajo. La actividad de tratamiento es tuya — el aviso que leerá la familia lo firmas tú como responsable de ese tratamiento — y se referencia por versión exacta.

<CodeGroup tag="POST" label="/shared-campaigns">

```bash {{ title: 'cURL' }}
curl https://app.edugoverna.com/api/partner/v1/organizations/{workspaceOrgId}/shared-campaigns \
  -H "Authorization: Bearer {keyId}.{secret}" \
  -H "Content-Type: application/json" \
  -d '{
    "processingActivityVersionId": "3f7e6c9d-8f21-4b83-bb1c-5e2d9a447f10",
    "name": "Fotografías y videos · Temporada 2026",
    "channels": ["email", "whatsapp"],
    "endsAt": "2026-10-31T03:00:00Z",
    "reminderDayOffsets": [3, 7],
    "defaultSuccessRedirectUrl": "https://plataforma.ejemplo.cl/gracias",
    "redirectMode": "bridge",
    "dispatchMode": "bulk",
    "publicCompletionEnabled": true,
    "targets": [
      { "shareId": "b1f6a3d2-4c1e-4e29-9d5b-8a0f7c3e2d11" },
      { "shareId": "e77c2b90-15aa-4d0f-b3c8-6f4a9d2e8b03" }
    ]
  }'
```

```js
const response = await fetch(
  `https://app.edugoverna.com/api/partner/v1/organizations/${workspaceOrgId}/shared-campaigns`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${keyId}.${secret}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      processingActivityVersionId: "3f7e6c9d-8f21-4b83-bb1c-5e2d9a447f10",
      name: "Fotografías y videos · Temporada 2026",
      channels: ["email", "whatsapp"],
      endsAt: "2026-10-31T03:00:00Z",
      reminderDayOffsets: [3, 7],
      defaultSuccessRedirectUrl: "https://plataforma.ejemplo.cl/gracias",
      redirectMode: "bridge",
      dispatchMode: "bulk",
      publicCompletionEnabled: true,
      targets: [
        { shareId: "b1f6a3d2-4c1e-4e29-9d5b-8a0f7c3e2d11" },
        { shareId: "e77c2b90-15aa-4d0f-b3c8-6f4a9d2e8b03" },
      ],
    }),
  }
)

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

</CodeGroup>

Los campos que definen el comportamiento:

- `targets` — obligatorio, 1 a 50 shares. Cada entrada puede además acotar por RUT (`ruts`, hasta 500 por share y 1.000 por campaña): en ese _modo RUT_ solo se contacta a quienes calcen dentro del grupo compartido, y ninguna respuesta revela cuáles calzaron.
- `dispatchMode` — `"bulk"` (por defecto) materializa y despacha el roster completo al publicar; `"per_subject"` materializa el roster pero espera a que tú emitas cada sobre.
- `channels` — `["email"]` por defecto; acepta `email` y `whatsapp`. `whatsapp` se envía desde el número de tu propio espacio de trabajo: debe estar conectado en Ajustes → WhatsApp, con mensajes aprobados por Meta; si no, `409 WHATSAPP_CHANNEL_UNAVAILABLE` (`details.reason`: `not_licensed`, `not_connected`, `setup_pending`, `setup_failed`, `disconnected`, `account_restricted`, `not_funded`, `templates_pending` o `spend_limit_reached`).
- `reminderDayOffsets` — hasta 3 recordatorios, en días 1–30 desde el envío y estrictamente crecientes.
- `redirects` — pares `{ rut, successRedirectUrl, declineRedirectUrl?, redirectMode? }` (hasta 2.500) que fijan un destino _por estudiante_ desde la creación. Los RUT que no calcen se ignoran en silencio; la respuesta solo repite cuántos pares enviaste.
- `publicCompletionEnabled` — abre la puerta pública de completación para titulares del padrón sin canal registrado: verifican su RUT, registran un contacto propio y validan con OTP.
- `endsAt` — cierre de la campaña; debe ser una fecha futura. Puedes extenderlo o adelantarlo después, ya publicada, con [`PATCH /shared-campaigns/:campaignId`](/campanas-compartidas#cambiar-el-cierre-de-una-campana). Si tu proceso termina antes, [ciérrala](/campanas-compartidas#cerrar-una-campana-compartida): las pendientes se anulan al instante y dejan de salir recordatorios. La campaña **no** se cierra sola en `endsAt`; solo vencen sus pendientes.

```json {{ title: 'Respuesta · 202 Accepted' }}
{
  "campaignId": "a4c8e2f6-7b31-4d95-8e0a-1f6c3b9d2e57",
  "submittedShareCount": 2,
  "publicCompletionEnabled": true,
  "publicCompletionUrl": "https://app.edugoverna.com/consents/campaigns/…",
  "status": "accepted"
}
```

<Note>
  La respuesta es deliberadamente opaca: repite lo que tú enviaste
  (`submittedShareCount`, `submittedRutCount`, `submittedRedirectCount`) y nunca
  cuántos titulares resolvieron ni sus contactos. Ese contrato de no divulgación
  protege el padrón del colegio.
</Note>

El estado agregado vive en `GET /shared-campaigns/:campaignId`: `origin` (`operator` para las que lanzas tú), estado, canales, `outcomes` (decisiones agregadas) y — salvo en modo RUT, donde se reporta `decidedOnly: true` — `rosterTotal` y `coverage`.

## 3. Emite sobres y reenvíos

En modo `per_subject`, cada envío es un _sobre_: una llamada que despacha la solicitud de un titular con su propio destino de redirección. Los sobres solo existen en campañas creadas con `dispatchMode: "per_subject"` (en `bulk` el despacho ya ocurrió al publicar, y la llamada responde `409 CONSENT_CAMPAIGN_DISPATCH_MODE_UNSUPPORTED`) y no están disponibles en campañas acotadas por RUT, que no divulgan estado por titular. Acepta un sobre individual o un lote de hasta 200:

<CodeGroup tag="POST" label="/shared-campaigns/:campaignId/envelopes">

```bash {{ title: 'cURL' }}
curl https://app.edugoverna.com/api/partner/v1/organizations/{workspaceOrgId}/shared-campaigns/{campaignId}/envelopes \
  -H "Authorization: Bearer {keyId}.{secret}" \
  -H "Content-Type: application/json" \
  -d '{
    "rut": "12345678-5",
    "successRedirectUrl": "https://plataforma.ejemplo.cl/matricula/8841/ok",
    "redirectMode": "auto",
    "partnerExternalId": "matricula-8841",
    "idempotencyKey": "sobre-matricula-8841"
  }'
```

```js
const response = await fetch(
  `https://app.edugoverna.com/api/partner/v1/organizations/${workspaceOrgId}/shared-campaigns/${campaignId}/envelopes`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${keyId}.${secret}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      envelopes: [
        {
          rut: "12345678-5",
          successRedirectUrl: "https://plataforma.ejemplo.cl/matricula/8841/ok",
        },
        { subjectId: "8f3b7f0a-2f9d-4d4e-a1c3-77b2a0f1d942" },
      ],
    }),
  }
)

const { results } = await response.json()
```

</CodeGroup>

Cada sobre identifica al titular con **exactamente uno** de `subjectId` (el id que devuelve la lectura del padrón compartido) o `rut`. La respuesta `202` trae un resultado por sobre:

```json {{ title: 'Resultado por sobre' }}
{
  "subjectId": "8f3b7f0a-2f9d-4d4e-a1c3-77b2a0f1d942",
  "rut": null,
  "envelopeId": "6c2e9a41-0d5f-4b83-a7e2-3f9c1b8d6e04",
  "consentRequestId": "0d9a2c1e-6d61-4f1c-9f5a-3f2f8b6f1a77",
  "status": "sent"
}
```

Los estados posibles son `sent`, `already_sent` (ese titular ya tiene su solicitud: reintentar es seguro), `not_in_campaign` (no está en el roster; como sobre individual responde `404 CONSENT_CAMPAIGN_CONTACT_NOT_FOUND`) y `share_revoked` (el colegio revocó el share del que venía).

El estado de todos los sobres — incluido el destino planificado de los que aún no se despachan — está en `GET .../envelopes`, filtrable por `status`. Para reenviar uno ya despachado, con la opción de corregir su destino:

```bash {{ title: 'Reenviar un sobre' }}
curl https://app.edugoverna.com/api/partner/v1/organizations/{workspaceOrgId}/shared-campaigns/{campaignId}/envelopes/{envelopeId}/resend \
  -H "Authorization: Bearer {keyId}.{secret}" \
  -H "Content-Type: application/json" \
  -d '{ "successRedirectUrl": "https://plataforma.ejemplo.cl/matricula/8841/ok-v2" }'
```

## 4. Sincroniza la audiencia cuando el padrón crece

Si el colegio matricula estudiantes nuevos en un curso compartido después de creada la campaña, no vuelvas a crearla: `sync-audience` re-resuelve los cursos guardados y agrega **solo** los contactos que faltan. Los `redirects` del cuerpo aplican a los agregados en esa pasada.

<CodeGroup tag="POST" label="/shared-campaigns/:campaignId/sync-audience">

```bash {{ title: 'cURL' }}
curl https://app.edugoverna.com/api/partner/v1/organizations/{workspaceOrgId}/shared-campaigns/{campaignId}/sync-audience \
  -H "Authorization: Bearer {keyId}.{secret}" \
  -H "Content-Type: application/json" \
  -d '{
    "redirects": [
      { "rut": "23456789-6", "successRedirectUrl": "https://plataforma.ejemplo.cl/matricula/9102/ok" }
    ]
  }'
```

```js
let pending = Infinity
while (pending > 0) {
  const response = await fetch(
    `https://app.edugoverna.com/api/partner/v1/organizations/${workspaceOrgId}/shared-campaigns/${campaignId}/sync-audience`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${keyId}.${secret}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({}),
    }
  )
  const result = await response.json()
  pending = result.pendingRequestCount
}
```

</CodeGroup>

```json {{ title: 'Respuesta · 202 Accepted' }}
{
  "addedContactCount": 4,
  "materializedRequestCount": 4,
  "pendingRequestCount": 0,
  "submittedRedirectCount": 1
}
```

En una campaña `bulk` publicada, la sincronización además emite y despacha las solicitudes nuevas por tramos: `pendingRequestCount > 0` significa «vuelve a llamar». El comando es repetible — cada pasada trabaja por diferencia contra lo ya escrito, así que reintentar nunca duplica.

## 5. Redirects: `auto` versus `bridge`

Tras decidir en el portal, el titular puede aterrizar en tu plataforma. En ambos modos el destino recibe `consent_id` y `decision` como parámetros de consulta. El modo controla _cómo_:

- **`auto`** (por defecto): confirmada la decisión, el portal redirige por sí solo tras una breve confirmación visual. Es el modo para flujos donde el consentimiento es un paso intermedio — una matrícula en tu plataforma que continúa del otro lado. Si el navegador del titular pide movimiento reducido, el portal muestra el botón manual en su lugar.
- **`bridge`**: el portal muestra una página puente con la confirmación de la decisión y un botón «Continuar» que el titular pulsa cuando quiera. Es el modo para cierres tranquilos, donde no hay un paso siguiente urgente.

En una campaña compartida los destinos son tuyos — el aviso que la familia lee lo firma tu organización — y hay tres momentos para fijarlos: al crear (`defaultSuccessRedirectUrl` y los pares `redirects` por RUT), al emitir o reenviar un sobre (destino de esa entrega puntual) y con `POST .../redirects` para corregir el destino planificado de sobres **aún no despachados** (para uno ya enviado, usa el reenvío con destino nuevo).

## 6. Seguimiento con webhooks

Los eventos de decisión de una campaña compartida cruzan dos organizaciones: la solicitud vive en el padrón del colegio, pero tú eres quien debe empezar — o dejar de — tratar datos con esa decisión. Por eso `consent.decision.recorded` se despacha a **ambas** partes, y tu copia llega con una proyección reducida (`projection: "shared_partner"`) limitada a lo que ya puedes leer por la API:

```json {{ title: 'Payload · proyección shared_partner' }}
{
  "event": "consent.decision.recorded",
  "sourceType": "consent_request",
  "sourceId": "0d9a2c1e-6d61-4f1c-9f5a-3f2f8b6f1a77",
  "occurredAt": "2026-08-25T14:03:22.000Z",
  "data": {
    "projection": "shared_partner",
    "consentRequestId": "0d9a2c1e-6d61-4f1c-9f5a-3f2f8b6f1a77",
    "dataOwnerOrganizationId": "org-colegio",
    "registryShareId": "b1f6a3d2-4c1e-4e29-9d5b-8a0f7c3e2d11",
    "campaignId": "a4c8e2f6-7b31-4d95-8e0a-1f6c3b9d2e57",
    "campaignStatus": "published",
    "campaignMode": "targeted",
    "campaignContactEntryId": "6c2e9a41-0d5f-4b83-a7e2-3f9c1b8d6e04",
    "processingActivityVersionId": "3f7e6c9d-8f21-4b83-bb1c-5e2d9a447f10",
    "subject": {
      "referenceCode": "EST-2026-0142",
      "subjectType": "student",
      "requestedForAgeBand": "under_14"
    },
    "status": "granted",
    "decisionActorType": "guardian",
    "requestedAt": "2026-08-25T13:40:12.000Z",
    "expiresAt": null,
    "updatedAt": "2026-08-25T14:03:22.000Z",
    "latestDecision": {
      "decision": "granted",
      "decisionAuthority": "guardian",
      "captureMethod": "portal_link_otp",
      "decidedAt": "2026-08-25T14:03:21.000Z",
      "effectiveFrom": "2026-08-25T14:03:21.000Z",
      "effectiveTo": null,
      "revokedAt": null
    }
  }
}
```

Correlaciona con `campaignContactEntryId` — el id del sobre en tu propia organización — en lugar de identificadores del padrón ajeno. La proyección nunca incluye RUT, contactos ni el nombre del titular: si los necesitas para mostrar, ya los tienes por `GET /shares/:shareId/subjects`.

Ten presente que las solicitudes de una campaña compartida viven en la organización del colegio, así que los eventos de su ciclo de entrega (`consent.requested`, `consent.delivery.*`) se enrutan a los endpoints del colegio, no a los tuyos: la copia que te llega a ti es la decisión. Para el estado intermedio de tus envíos usa `GET /shared-campaigns/:campaignId` y `GET .../envelopes`. Configura tus endpoints en [Gestión » Webhooks](/gestion/webhooks) y revisa el formato general en [Webhooks](/webhooks).

<Note>
  Si el padrón vino de un [registrador externo](/guias/registrador-externo) que
  declaró consentimientos ya firmados ante el colegio, el share trae además una
  campaña **declarada** (`origin: "registrar_attestation"`) que no creaste tú:
  no envía mensajes y es de sólo lectura. Sincronizar su audiencia responde
  `409 CONSENT_CAMPAIGN_ATTESTED_READ_ONLY`, y esas decisiones llegan en
  `consent.decision.recorded` con `captureMethod` igual a `registrar_attestation`. Ver [Consentimiento
  declarado](/guias/registrador-externo#consentimiento-declarado).
</Note>

## ¿Qué sigue?

- [Flujo de consentimiento de principio a fin](/guias/flujo-consentimiento), para el detalle de lo que vive cada familia
- [Campañas compartidas: modelo y garantías](/campanas-compartidas)
- [Padrón y proyecciones de lectura](/padron)
- [Errores](/errores) y [límites de tasa](/limites)
