# Pagination

> Lists are paged with limit and an opaque cursor: the response includes next_cursor, which you pass back as cursor.

Page: https://safeonuba.com/en/developers/pagination · SafeOnuba API v1 (`api_version: "2026-09-24"`), in preview: the contract may change.

The API’s lists (`/v1/alerts`, `/v1/musters`, `/v1/workers`, `/v1/devices`, `/v1/events`) are walked with a **cursor**.

## How it works

| Parameter | What it is |
| --- | --- |
| `limit` | Items per page, from 1 to 500. 100 by default. |
| `cursor` | The `next_cursor` from the previous page. On the first page, it isn’t sent. |

Each response carries the page in `data` and a `next_cursor`:

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

For the next page, repeat the same request with the same filters and `cursor=<next_cursor>`. When `next_cursor` is `null`, there are no more.

> **The cursor is opaque**
>
> Don’t interpret it or build it: pass it exactly as it is.

## Walking a whole list

```javascript
async function* fetchAll(path, filters = {}) {
  let cursor = null;
  do {
    const url = new URL(`https://sandbox.api.safeonuba.com${path}`);
    for (const [k, v] of Object.entries(filters)) 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 safeOnubaToken()}` } });
    if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
    const page = await res.json();
    yield* page.data;
    cursor = page.next_cursor;
  } while (cursor);
}

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

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

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

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

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

## Order of each list

- `/v1/alerts`: from the most recent to the oldest.
- `/v1/events`: from the oldest to the most recent, in publication order. That way it replays in the same order in which it was sent.

## What a page costs

A page of 500 items counts for more towards your query-time limit than one of 50 (see [Rate limits](https://safeonuba.com/en/developers/rate-limits)). Ask for what you’re going to use.

The short lists that aren’t paginated (`/v1/sites`, `/v1/sites/{id}/zones`, `/v1/musters/{id}/missing`, `/v1/positions/latest`, `/v1/webhooks`) return everything in `data`, with no `next_cursor`.
