---
title: "Importaciones"
description: "El importador inteligente por API: una planilla pasa por analyze → preview → commit, con contrato de checksum entre pasos."
url: "https://docs.edugoverna.com/api-organizacion/importaciones"
---
# Importaciones

El importador puebla el padrón desde **una sola planilla** (CSV o XLSX) con estudiantes, apoderados, Cursos y Generación en las mismas filas. El flujo tiene tres pasos sin estado en el servidor — `analyze` detecta columnas y propone un mapeo, `preview` simula, `commit` escribe — y un **contrato de checksum** garantiza que los tres pasos hablan del mismo archivo. Todas las rutas exigen el módulo `import` del plan (si no está licenciado, `403 MODULE_NOT_LICENSED`). {{ className: 'lead' }}

<Note>
  Los permisos se derivan **del mapeo, no del archivo**: `subjects.write`
  siempre; si el mapeo incluye columnas de apoderado, además `guardians.write`;
  si asigna Cursos o Generación, además `students.write`. Un mapeo que tu key no
  puede ejecutar responde `403 AUTHORIZATION_REQUIRED` con `details.permission`
  indicando cuál falta.
</Note>

## El contrato de checksum

`analyze` calcula el SHA-256 del archivo y lo devuelve como `analysis.fileChecksum`, ya embebido en `analysis.suggestedMapping`. `preview` y `commit` reciben el archivo **de nuevo** junto con el mapeo, recalculan el hash y lo comparan con `mapping.fileChecksum`: si no coinciden, `422 IMPORT_FILE_CHANGED`. No hay sesión de importación ni `uploadId` — el commit re-ejecuta todo el pipeline y nunca confía en un preview anterior; el checksum es la única (y suficiente) garantía de que no se comprometen bytes distintos de los analizados.

---

## Analizar la planilla {{ tag: 'POST', label: '/api/imports/analyze' }}

<Row>
  <Col>

    Requiere `subjects.write`. `multipart/form-data` con un único campo `file` (CSV o XLSX, máximo 5 MB). Detecta delimitador y codificación, clasifica cada columna con su confianza, reconoce la forma de los apoderados (`wide`: columnas apoderado 1/2; `long`: una fila por vínculo) y detecta si los nombres de hoja parecen Cursos (`1°A`, `1°B`). La joya es `suggestedMapping`: un mapeo completo listo para reenviar a `preview`.

    ### Errores propios

    <Properties>
      <Property name="IMPORT_FILE_TYPE_UNSUPPORTED" type="400">
        Ni CSV ni XLSX.
      </Property>
      <Property name="IMPORT_FILE_TOO_LARGE" type="400">
        Más de 5 MB.
      </Property>
    </Properties>

  </Col>
  <Col sticky>

    <CodeGroup title="Solicitud" tag="POST" label="/api/imports/analyze">

    ```bash {{ title: 'cURL' }}
    curl -X POST "https://app.edugoverna.com/api/imports/analyze" \
      -H "x-api-key: {tu_api_key}" \
      -F "file=@matriculas-2026.xlsx"
    ```

    ```js
    const form = new FormData()
    form.append('file', archivo) // File o Blob

    const { analysis } = await fetch(
      'https://app.edugoverna.com/api/imports/analyze',
      {
        method: 'POST',
        headers: { 'x-api-key': process.env.EDUGOVERNA_API_KEY },
        body: form,
      },
    ).then((r) => r.json())
    ```

    </CodeGroup>

    ```json {{ title: 'Respuesta (recortada)' }}
    {
      "analysis": {
        "fileChecksum": "0f2a…64-hex…",
        "kind": "xlsx",
        "delimiter": null,
        "encoding": "utf-8",
        "sheets": [
          {
            "name": "1°A",
            "rowCount": 33,
            "headers": ["RUT", "Nombres", "Apellidos", "…"],
            "suggestedInclude": true,
            "sheetNameLooksLikeCurso": true
          }
        ],
        "columns": [
          {
            "sourceIndex": 0,
            "sourceHeader": "RUT",
            "suggestedTarget": "student.rut",
            "confidence": "high",
            "alternatives": []
          }
        ],
        "guardianShape": { "detected": "wide", "confidence": "high" },
        "suggestedMapping": {
          "fileChecksum": "0f2a…64-hex…",
          "guardianShape": "wide",
          "sheets": [{ "name": "1°A", "include": true }],
          "columns": [{ "sourceIndex": 0, "sourceHeader": "RUT", "target": "student.rut" }],
          "cursoSource": { "type": "sheetName" },
          "generacion": { "type": "none" },
          "defaults": { "relationshipType": "apoderado" }
        },
        "warnings": []
      }
    }
    ```

  </Col>
</Row>

---

## Previsualizar {{ tag: 'POST', label: '/api/imports/preview' }}

<Row>
  <Col>

    `multipart/form-data` con `file` (los mismos bytes) y `mapping` (el `ImportMapping` como **string JSON** — normalmente el `suggestedMapping`, ajustado). Simula la importación completa sin escribir nada: totales de altas/actualizaciones/errores, el veredicto fila a fila, y el **plan de Cursos** — qué nombre de origen cae en qué segmento existente y cuáles habría que crear.

    Para que `commit` pueda crear un Curso nuevo, su `sourceName` debe venir autorizado en `mapping.approvedCursoCreations`; si no, `422 IMPORT_CURSO_NOT_APPROVED`. Así una hoja mal nombrada no acuña cursos por accidente.

    Si la planilla trae el RUN y el dígito verificador en columnas separadas (como exporta SIGE), mapea la segunda a `student.checkDigit` o `guardian[N].checkDigit`: el RUT se compone como `RUN-DV`. Las filas con una sola celda con texto («Total alumnos: 14») no son filas de datos: se omiten y se cuentan en `skippedNoteRows`. Una fila con error que no alcanzó a producir un estudiante trae `rawRut` y `rawStudentName` tal como venían, para ubicarla en la planilla.

  </Col>
  <Col sticky>

    <CodeGroup title="Solicitud" tag="POST" label="/api/imports/preview">

    ```bash {{ title: 'cURL' }}
    curl -X POST "https://app.edugoverna.com/api/imports/preview" \
      -H "x-api-key: {tu_api_key}" \
      -F "file=@matriculas-2026.xlsx" \
      -F 'mapping={"fileChecksum":"0f2a…", "guardianShape":"wide", …}'
    ```

    ```js
    const form = new FormData()
    form.append('file', archivo)
    form.append('mapping', JSON.stringify(analysis.suggestedMapping))

    const { preview } = await fetch(
      'https://app.edugoverna.com/api/imports/preview',
      {
        method: 'POST',
        headers: { 'x-api-key': process.env.EDUGOVERNA_API_KEY },
        body: form,
      },
    ).then((r) => r.json())
    ```

    </CodeGroup>

    ```json {{ title: 'Respuesta (recortada)' }}
    {
      "preview": {
        "totals": {
          "students": { "new": 31, "update": 2, "error": 0 },
          "guardians": { "new": 54, "update": 3, "error": 1 },
          "guardianships": { "new": 55, "update": 0, "error": 1 }
        },
        "rows": [
          {
            "sheetName": "1°A",
            "line": 2,
            "status": "new",
            "rut": "23456789-6",
            "studentName": "Martina Rojas Fuentes",
            "guardianNames": ["Carolina Fuentes"],
            "cursoName": "1°A",
            "warnings": []
          }
        ],
        "cursoPlan": [
          { "sourceName": "1°A", "resolution": { "type": "create" }, "studentCount": 33 }
        ],
        "ignoredColumns": [],
        "skippedNoteRows": 0,
        "sheetErrors": []
      }
    }
    ```

  </Col>
</Row>

---

## Confirmar {{ tag: 'POST', label: '/api/imports/commit' }}

<Row>
  <Col>

    Mismo cuerpo que `preview` más el campo opcional `rowStart`: el commit escribe en **tramos de 40 filas** y `result.progress` te dice si queda otro tramo por pedir. El resultado detalla fila a fila qué se creó, actualizó o falló, más los reportes de Cursos y Generación.

  </Col>
  <Col sticky>

    <CodeGroup title="Solicitud" tag="POST" label="/api/imports/commit">

    ```bash {{ title: 'cURL' }}
    curl -X POST "https://app.edugoverna.com/api/imports/commit" \
      -H "x-api-key: {tu_api_key}" \
      -F "file=@matriculas-2026.xlsx" \
      -F 'mapping={"fileChecksum":"0f2a…", …}' \
      -F "rowStart=0"
    ```

    ```js
    let rowStart = 0
    let done = false
    while (!done) {
      const form = new FormData()
      form.append('file', archivo)
      form.append('mapping', JSON.stringify(mapping))
      form.append('rowStart', String(rowStart))

      const { result } = await fetch(
        'https://app.edugoverna.com/api/imports/commit',
        {
          method: 'POST',
          headers: { 'x-api-key': process.env.EDUGOVERNA_API_KEY },
          body: form,
        },
      ).then((r) => r.json())

      done = result.progress.done
      rowStart = result.progress.rangeEnd
    }
    ```

    </CodeGroup>

    ```json {{ title: 'Respuesta (recortada)' }}
    {
      "result": {
        "students": { "created": 31, "updated": 2, "failed": 0, "skipped": 0 },
        "guardians": { "created": 54, "updated": 3, "failed": 1 },
        "guardianships": { "created": 55, "updated": 0, "failed": 1 },
        "rows": [
          {
            "sheetName": "1°A",
            "line": 2,
            "outcome": "created",
            "subjectId": "3f6f…",
            "rut": "23456789-6",
            "warnings": []
          }
        ],
        "issues": [],
        "cursoReport": [
          { "sourceName": "1°A", "segmentId": "5b2c…", "created": true, "added": 33 }
        ],
        "progress": { "totalRows": 33, "rangeStart": 0, "rangeEnd": 33, "done": true }
      }
    }
    ```

  </Col>
</Row>

---

## Plantilla oficial {{ tag: 'GET', label: '/api/imports/template.xlsx' }}

`GET /imports/template.xlsx` descarga la planilla de ejemplo (`plantilla-estudiantes.xlsx`). Está garantizado por contrato que la plantilla pasa por `analyze` sin advertencias y con todas sus columnas detectadas en confianza `high` — es el punto de partida recomendado para los colegios.

---

## Padrones compartidos y registradores externos

Cuando quien tiene la nómina no es el colegio (un club, una academia, un fotógrafo), el padrón entra por **registradores externos**: se les envía una invitación y ellos cargan su registro en un portal público con el mismo importador. Desde la API de organización se administra la relación, no la carga:

| Método y ruta                                | Permiso                    | Qué hace                                                                                                   |
| -------------------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `GET /registrar-invites`                     | `registrar_invites.manage` | Invitaciones enviadas, con estado (`pending`, `expired`, aceptadas con su `registryOrganizationId`).       |
| `POST /registrar-invites`                    | `registrar_invites.manage` | Invita a un registrador externo por correo. Acepta `processingActivityId` opcional (actividad de consentimiento propia, publicada y no granular) para que el registrador declare [consentimientos ya firmados](/guias/registrador-externo#consentimiento-declarado). |
| `POST /registrar-invites/:inviteId/resend`   | `registrar_invites.manage` | Reenvía la invitación.                                                                                     |
| `DELETE /registrar-invites/:inviteId`        | `registrar_invites.manage` | Cancela la invitación.                                                                                     |
| `GET /registry-shares`                       | `registry_shares.read`     | Los padrones que tú compartiste como dueño: grupo, estado, `memberCount`, campañas y solicitudes asociadas; los compartidos contigo llegan por `GET /shared-sources`. |
| `POST /registry-shares` · `PATCH` · `DELETE` | `registry_shares.write`    | Otorga, ajusta o revoca un share (por Curso: N shares por registro). El `PATCH` acepta `{ "nationalIdDisclosureEnabled": boolean }` — el interruptor de divulgación del RUT completo que gobierna `disclosure=full` en la API Partner. |
| `GET /shared-sources`                        | `shared_subjects.read`     | Las fuentes compartidas y sus titulares, para campañas sobre padrón ajeno.                                 |

Las campañas de consentimiento sobre padrones compartidos están documentadas en [Consentimientos](/api-organizacion/consentimientos) y en la [guía de campañas compartidas](/guias/campanas-compartidas).
