Paginación

Cómo recorrer listados largos sin perder ni repetir registros.

La API tiene tres estilos de paginación conviviendo —y varias formas distintas de respuesta entre ellos—. No es una decisión de diseño: es historia. Antes de recorrer un listado, mirá en la API Reference qué parámetros acepta ese endpoint y qué esquema devuelve. Abajo está la tabla de cuál usa cada uno.

Estilo 1 — limit y offset

Es el que usan los endpoints más nuevos, entre ellos los de la sección organizations y los de usuarios.

$curl "https://www.crehana.com/api/v5/rest/org/acme/organizations/areas/?limit=50&offset=0" \
> -H "Api-Key: $API_KEY" -H "Secret-Access: $SECRET_ACCESS"

La respuesta trae el total de registros y la página pedida:

1{
2 "total": 214,
3 "results": [ ]
4}

Para avanzar, sumás limit al offset: offset=50, offset=100, y así. Terminás cuando results trae menos elementos que el limit pedido.

Estos endpoints no devuelven la envoltura de message / code / data: total y results están en la raíz de la respuesta. Es la diferencia que más sorprende al escribir el primer cliente, porque el mismo código no sirve para leer un listado y para leer una operación de escritura.

El techo de limit no es el mismo en todos los endpoints, y pedir más devuelve 422. Los de organizations y los de timeoff cortan en 50, los de usuarios y los reportes de learning en 100, y el reporte general v2 y los cuatro de Asistencia admiten hasta 500. El valor por defecto tampoco es único: en varios es 10 y en los reportes es 100. Cada operación dice su techo y su valor por omisión en la API Reference; si no querés mirarlo, mandá limit explícito y quedate abajo de 50.

Este estilo es sensible a los cambios durante el recorrido. Si mientras paginás se da de alta un usuario, los registros se corren y podés ver uno repetido o saltearte otro. Para exportaciones grandes conviene hacerlas en una ventana de poca actividad.

Estilo 2 — cursor y first

Es el que usan los endpoints construidos sobre conexiones, entre ellos los deprecados de estructura organizacional y los grupos de permisos.

$curl "https://www.crehana.com/api/v5/rest/org/acme/permission_groups?first=50" \
> -H "Api-Key: $API_KEY" -H "Secret-Access: $SECRET_ACCESS"

La respuesta trae los campos que necesitás para seguir:

1{
2 "data": [ ],
3 "next_cursor": "YXJyYXljb25uZWN0aW9uOjQ5",
4 "has_next_page": true,
5 "total_count": 214
6}

Mientras has_next_page sea true, repetís la llamada pasando cursor=<next_cursor>. Este estilo no se corre si los datos cambian a mitad de camino: el cursor apunta a una posición estable.

Los endpoints del catálogo de contenidos también se paginan con first y cursor, pero la respuesta viene con otra forma: la conexión sin aplanar. Los registros están en edges, cada uno dentro de un node, y los datos para seguir están en page_info:

1{
2 "total_count": 214,
3 "page_info": {
4 "has_next_page": true,
5 "has_previous_page": false,
6 "start_cursor": "YXJyYXljb25uZWN0aW9uOjA=",
7 "end_cursor": "YXJyYXljb25uZWN0aW9uOjQ5"
8 },
9 "edges": [
10 { "node": { } }
11 ]
12}

El recorrido es el mismo, con los campos en otro lugar: mientras page_info.has_next_page sea true, repetís pasando cursor=<page_info.end_cursor>. Y para leer un registro hay que bajar un nivel más, a edges[].node.

Estilo 3 — limit y cursor (reportes de Asistencia)

Los reportes de Recepción de marcas y Gestión diaria de marcas paginan con un cursor, pero la respuesta conserva la misma forma que el resto de los reportes: total y results en la raíz, más dos campos para seguir.

$curl "https://www.crehana.com/api/v5/rest/org/acme/reports/attendance/marks/?from_date=2026-07-20&to_date=2026-08-19&limit=50" \
> -H "Api-Key: $API_KEY" -H "Secret-Access: $SECRET_ACCESS"
1{
2 "total": 42,
3 "results": [ ],
4 "next_cursor": "YXR0ZW5kYW5jZTpbIjIwMjYtMDctMjAiLC...",
5 "has_next_page": true
6}

Mientras has_next_page sea true, repetís la misma llamada agregando cursor=<next_cursor>. El cursor es opaco: mandalo tal cual vino, sin decodificarlo ni construirlo vos.

Los otros dos reportes de Asistencia —Creación de turnos y Programación de turnos— siguen con limit y offset, como el resto de los reportes. Son catálogos de cientos de filas, no de millones.

Por qué estos dos son distintos

Marcas y jornadas crecen con colaboradores × días: en una organización grande son millones de filas. Con offset, cada página obliga a la base a producir y descartar todas las filas anteriores, así que bajar el reporte completo se vuelve cada vez más lento a medida que avanzás. El cursor apunta a la última fila entregada, de modo que la página siguiente arranca ahí y cuesta lo mismo sea la primera o la número doscientos.

Estos dos endpoints no aceptan offset. Si lo mandás, la respuesta es 400. No se ignora en silencio a propósito: si lo hiciera, recibirías siempre la primera página creyendo que estás avanzando.

La paginación es solo hacia adelante. No hay cursor previo ni has_previous_page: no se puede retroceder ni saltar a una página arbitraria. Si necesitás una porción puntual del reporte, acotá from_date y to_date en vez de paginar hasta ahí.

El recorrido completo

$# Página 1: sin cursor
$curl ".../reports/attendance/marks/?from_date=2026-07-20&to_date=2026-08-19&limit=50"
$
$# Guardás next_cursor de esa respuesta y lo mandás en la siguiente
$curl ".../reports/attendance/marks/?from_date=2026-07-20&to_date=2026-08-19&limit=50&cursor=<next_cursor>"
$
$# Repetís mientras has_next_page sea true.
$# En la última página next_cursor viene en null y has_next_page en false.

El cursor está atado a la consulta que lo generó. Si cambiás from_date, to_date, limit o el filtro por colaborador, empezá de nuevo desde la primera página: un cursor de otra consulta devuelve 400.

Cuál usa cada endpoint

En vez de memorizarlo, miralo en la API Reference: los parámetros de query están listados en cada operación, y el esquema de la respuesta dice cuál forma devuelve. Como regla aproximada:

Si el endpoint…PaginaciónForma de la respuesta
Está bajo /organizations/, /users/user-organizations/, /reports/ o /timeoff/limit + offsettotal + results
Es /reports/attendance/marks/ o /reports/attendance/daily-management/limit + cursortotal + results + next_cursor
Está bajo /learning/content/knowledge-hub/catalog/first + cursortotal_count + page_info + edges
Es /users/, /users/state/ o /custom-fields/first + cursordata + next_cursor
Está marcado como deprecadofirst + cursormirar la operación

Recomendaciones

  • No asumas un tamaño de página por defecto. Pedí siempre limit o first de forma explícita.
  • Frená por la respuesta, no por un contador. Usá has_next_page, o que la página venga incompleta. Calcular la cantidad de páginas a partir de total_count falla si los datos cambian.
  • Guardá el cursor, no el número de página. En los estilos 2 y 3 el cursor es lo único que garantiza continuidad.
  • No construyas ni interpretes un cursor. Es opaco y su formato puede cambiar sin aviso: reenviá exactamente el que te devolvió la respuesta.