How it works
Pagination
Lists are paged with limit and an opaque cursor: the response includes next_cursor, which you pass back as cursor.
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:
{
"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.
Walking a whole list
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);
}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"])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). 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.