# Paginación

> Las listas se recorren con limit y un cursor opaco: la respuesta trae next_cursor y se pasa como cursor.

Página: https://safeonuba.com/desarrolladores/paginacion · API de SafeOnuba v1 (`api_version: "2026-09-24"`), en vista previa: el contrato puede cambiar.

Las listas de la API (`/v1/alerts`, `/v1/musters`, `/v1/workers`, `/v1/devices`, `/v1/events`) se recorren con un **cursor**.

## Cómo funciona

| Parámetro | Qué es |
| --- | --- |
| `limit` | Elementos por página, de 1 a 500. Por defecto, 100. |
| `cursor` | El `next_cursor` de la página anterior. En la primera página, no se manda. |

Cada respuesta trae la página en `data` y un `next_cursor`:

```json
{
  "data": [ … ],
  "next_cursor": "eyJ0IjoiMjAyNi0wOS0yNFQxMTowNDoxMloifQ"
}
```

Para la página siguiente, repite la misma petición con los mismos filtros y `cursor=<next_cursor>`. Cuando `next_cursor` es `null`, no hay más.

> **El cursor es opaco**
>
> No lo interpretes ni lo construyas: pásalo tal cual.

## Recorrer una lista entera

```javascript
async function* todas(ruta, filtros = {}) {
  let cursor = null;
  do {
    const url = new URL(`https://sandbox.api.safeonuba.com${ruta}`);
    for (const [k, v] of Object.entries(filtros)) url.searchParams.set(k, v);
    url.searchParams.set('limit', '100');
    if (cursor) url.searchParams.set('cursor', cursor);

    const res = await fetch(url, { headers: { Authorization: `Bearer ${await tokenSafeOnuba()}` } });
    if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
    const pagina = await res.json();
    yield* pagina.data;
    cursor = pagina.next_cursor;
  } while (cursor);
}

for await (const alerta of todas('/v1/alerts', { status: 'unacknowledged' })) {
  console.log(alerta.id, alerta.title);
}
```

```python
def todas(ruta: str, **filtros):
    cursor = None
    while True:
        params = {**filtros, "limit": 100}
        if cursor:
            params["cursor"] = cursor
        res = requests.get(
            f"https://sandbox.api.safeonuba.com{ruta}",
            headers={"Authorization": f"Bearer {token_safeonuba()}"},
            params=params,
            timeout=30,
        )
        res.raise_for_status()
        pagina = res.json()
        yield from pagina["data"]
        cursor = pagina["next_cursor"]
        if not cursor:
            return

for alerta in todas("/v1/alerts", status="unacknowledged"):
    print(alerta["id"], alerta["title"])
```

```csharp
async IAsyncEnumerable<JsonElement> Todas(string ruta, string filtros = "")
{
    string? cursor = null;
    do
    {
        var url = $"https://sandbox.api.safeonuba.com{ruta}?limit=100{filtros}"
                  + (cursor is null ? "" : $"&cursor={Uri.EscapeDataString(cursor)}");
        var res = await http.GetAsync(url);
        res.EnsureSuccessStatusCode();
        var pagina = await res.Content.ReadFromJsonAsync<JsonElement>();
        foreach (var elemento in pagina.GetProperty("data").EnumerateArray()) yield return elemento;
        var siguiente = pagina.GetProperty("next_cursor");
        cursor = siguiente.ValueKind == JsonValueKind.Null ? null : siguiente.GetString();
    } while (cursor is not null);
}

await foreach (var alerta in Todas("/v1/alerts", "&status=unacknowledged"))
    Console.WriteLine(alerta.GetProperty("title").GetString());
```

## Orden de cada lista

- `/v1/alerts`: de la más reciente a la más antigua.
- `/v1/events`: del más antiguo al más reciente, en orden de publicación. Así se reproduce en el mismo orden en que se envió.

## Lo que cuesta una página

Una página de 500 elementos cuenta más para tu límite de tiempo de consulta que una de 50 (ver [Límites de uso](https://safeonuba.com/desarrolladores/limites)). Pide lo que vas a usar.

Las listas cortas que no se paginan (`/v1/sites`, `/v1/sites/{id}/zones`, `/v1/musters/{id}/missing`, `/v1/positions/latest`, `/v1/webhooks`) devuelven todo en `data`, sin `next_cursor`.
