---
title: "Paginación"
description: "Cómo paginar las listas de la API de Edugoverna con cursores opacos, y cómo sincronizar cambios de forma incremental con el parámetro since."
url: "https://docs.edugoverna.com/paginacion"
---
# Paginación

Las listas de la Partner API se paginan por **cursor**: cada página incluye un `nextCursor` opaco que apunta a la página siguiente, hasta que llega como `null`. El cursor codifica una posición estable en el ordenamiento (no un número de página), así que el recorrido es determinista aunque se inserten filas nuevas mientras paginas. {{ className: 'lead' }}

## Cómo funciona

<Row>
  <Col>

    Toda lista acepta los mismos dos parámetros. El resto de los filtros
    depende de cada recurso.

    ### Parámetros de paginación

    <Properties>
      <Property name="limit" type="integer">
        Cuántos registros devolver por página. En las listas de
        consentimientos: entre 1 y 200, con 50 por defecto. En el padrón
        (`directory/subjects`): hasta 250, con 100 por defecto.
      </Property>
      <Property name="cursor" type="string">
        El `nextCursor` devuelto por la página anterior, sin modificar. Es un
        valor opaco: no lo parsees ni lo construyas.
      </Property>
    </Properties>

    El cursor está **ligado a la consulta que lo emitió**: al recurso, a la
    organización y a sus filtros. Reanudar un recorrido cambiando los filtros
    respondería filas incorrectas sin que pudieras notarlo, así que el servidor
    responde `422 INVALID_CURSOR`: en las listas de consentimientos, ante
    cualquier cambio de filtros u organización; en el padrón, cuando la
    posición a la que apunta el cursor ya no calza con la consulta. Si
    necesitas cambiar los filtros, empieza un recorrido nuevo desde la primera
    página.

    Un cursor tampoco expira por tiempo: puedes pausar un recorrido largo y
    retomarlo después, siempre con los mismos filtros.

  </Col>
  <Col sticky>

    <CodeGroup title="Primera página" tag="GET" label="/partner/v1/organizations/:organizationId/directory/subjects">

    ```bash {{ title: 'cURL' }}
    curl -G "https://app.edugoverna.com/api/partner/v1/organizations/{organizationId}/directory/subjects" \
      -H "Authorization: Bearer $EDUGOVERNA_PARTNER_KEY" \
      -d subjectType=student \
      -d limit=100
    ```

    ```js
    const url = new URL(
      `https://app.edugoverna.com/api/partner/v1/organizations/${organizationId}/directory/subjects`,
    )
    url.searchParams.set('subjectType', 'student')
    url.searchParams.set('limit', '100')

    const response = await fetch(url, {
      headers: { Authorization: `Bearer ${key}` },
    })
    const { subjects, meta } = await response.json()
    ```

    </CodeGroup>

    ```json {{ title: 'Respuesta (recortada)' }}
    {
      "subjects": [ "…100 titulares…" ],
      "meta": {
        "filteredRestrictedCount": 0,
        "totalMatchedCount": 412,
        "nextCursor": "eyJzY29wZSI6…"
      }
    }
    ```

  </Col>
</Row>

## Recorrer todas las páginas

<Row>
  <Col>

    Para recorrer una lista completa, repite la solicitud pasando el
    `nextCursor` de cada respuesta hasta recibir `null`. En el padrón y en
    los consentimientos de un grupo compartido el cursor viaja dentro de
    `meta`; en la lista de solicitudes de consentimiento viene al nivel
    superior de la respuesta, junto a `consentRequests`.

    Dos detalles del padrón que conviene conocer:

    - `totalMatchedCount` es el total de titulares que calzan con el filtro,
      no el tamaño de la página.
    - `filteredRestrictedCount` cuenta titulares que existían pero fueron
      excluidos de la respuesta — por una restricción de tratamiento activa o
      por la condición `onlyActive` del alcance de tu conexión. La suma de lo
      recibido puede ser menor que el total del colegio, y eso es
      intencional.

  </Col>
  <Col sticky>

    ```js {{ title: 'Recorrido completo' }}
    async function listAllSubjects(organizationId, key) {
      const subjects = []
      let cursor = null

      do {
        const url = new URL(
          `https://app.edugoverna.com/api/partner/v1/organizations/${organizationId}/directory/subjects`,
        )
        url.searchParams.set('limit', '100')
        if (cursor) url.searchParams.set('cursor', cursor)

        const response = await fetch(url, {
          headers: { Authorization: `Bearer ${key}` },
        })
        if (!response.ok) {
          const { error } = await response.json()
          throw new Error(`${error.code}: ${error.message}`)
        }

        const page = await response.json()
        subjects.push(...page.subjects)
        cursor = page.meta.nextCursor
      } while (cursor)

      return subjects
    }
    ```

  </Col>
</Row>

## Sincronización incremental con `since`

<Row>
  <Col>

    La lista de solicitudes de consentimiento acepta además un parámetro
    `since` pensado para trabajos de sincronización: devuelve solo las
    solicitudes **creadas después** de esa fecha (comparada contra
    `requestedAt`). Así, un trabajo periódico no necesita recorrer el
    histórico completo en cada corrida:

    1. En cada corrida, guarda la marca de tiempo de inicio.
    2. Consulta con `since` igual a la marca guardada de la corrida anterior
       y pagina con `cursor` hasta agotar los resultados.
    3. Si todo terminó bien, persiste la marca nueva.

    `since` es un filtro como cualquier otro: forma parte de la identidad del
    cursor, así que las páginas de un mismo recorrido deben repetirlo con el
    mismo valor.

    <Note>
      `since` filtra por fecha de **creación** de la solicitud, no de su
      último cambio. Para enterarte de decisiones y cambios de estado sin
      consultar en un ciclo, complementa la sincronización con los
      [webhooks](/webhooks) — por ejemplo `consent.decision.recorded`.
    </Note>

  </Col>
  <Col sticky>

    <CodeGroup title="Sincronización" tag="GET" label="/partner/v1/organizations/:organizationId/consents/requests">

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

    ```js
    const url = new URL(
      `https://app.edugoverna.com/api/partner/v1/organizations/${organizationId}/consents/requests`,
    )
    url.searchParams.set('since', lastSyncedAt) // ISO 8601
    url.searchParams.set('limit', '200')

    const response = await fetch(url, {
      headers: { Authorization: `Bearer ${key}` },
    })
    const { consentRequests, nextCursor } = await response.json()
    ```

    </CodeGroup>

    ```json {{ title: 'Respuesta (recortada)' }}
    {
      "consentRequests": [ "…solicitudes nuevas…" ],
      "nextCursor": null
    }
    ```

  </Col>
</Row>

## Otros filtros de la lista de consentimientos

Además de `since`, `cursor` y `limit`, la lista de solicitudes de consentimiento acepta:

<Properties>
  <Property name="status" type="string">
    Filtra por estado de la solicitud (por ejemplo `pending` o `granted`).
  </Property>
  <Property name="processingActivityCode" type="string">
    Solo solicitudes de una actividad de tratamiento, por su código.
  </Property>
  <Property name="partnerExternalId" type="string">
    Solo solicitudes que creaste con ese identificador externo tuyo — útil para
    reconciliar contra tu propio sistema.
  </Property>
</Properties>

El detalle de cada recurso y sus filtros está en la referencia: [sujetos del padrón](/api-organizacion/sujetos) y [consentimientos](/api-organizacion/consentimientos).
