Pagination and polling
Paginated lists
Section titled “Paginated lists”Paginated list endpoints take page and limit query parameters and return the records in
data, with paging details in meta.pagination:
{ "data": [{ "id": "…" }], "meta": { "pagination": { "page": 1, "limit": 50, "total": 132, "totalPages": 3, "hasNext": true, "hasPrev": false } }}pagestarts at 1.limitis the number of records per page. The default and maximum depend on the endpoint (see the table below).- Keep requesting the next
pagewhilehasNextistrue.
| Endpoint | Default limit |
Maximum limit |
Sorting | updatedAfter / updatedBefore |
|---|---|---|---|---|
GET /events |
50 | Use 100 or less | sort, dir |
Yes |
GET /action-items |
20 | 100 | — | Yes |
GET /metrics |
100 | 1000 | Always by updatedAt |
Yes |
GET /events/metrics/bulk |
100 | 1000 | Always by updatedAt |
Yes |
GET /workflows |
50 | 1000 | — | — |
GET /workflows/{workflowId}/runs |
50 | 1000 | — | — |
GET /assets, GET /locations, GET /users and GET /equipment-types are not
paginated: they return every matching record in data in one response.
Sorting events
Section titled “Sorting events”GET /events takes sort (a field name such as eventDate, createdAt or updatedAt)
and dir (asc or desc). By default events are sorted by eventDate, newest first.
Polling for changes
Section titled “Polling for changes”To keep another system in sync, don’t re-download everything. Ask for records that changed
since your last sync with updatedAfter, and save the newest updatedAt you received as
the starting point for the next sync.
This works on GET /events, GET /action-items, GET /metrics and
GET /events/metrics/bulk.
- On the first run, choose a starting time, for example the start of today, in ISO 8601
UTC:
2026-09-01T00:00:00Z. - Request
?updatedAfter=<cursor>&page=1. WhenupdatedAfteris set, results come back sorted byupdatedAt, oldest first. OnGET /events, sendsort=updatedAt&dir=ascas well, so the order stays right if you add other options. - Page through with
page=2, 3, …untilhasNextisfalse, and process each record. - Save the largest
updatedAtyou saw as the new cursor. - Wait, then repeat from step 2 with the new cursor.
curl "https://safe-api.safetyradar.com/v2/events?updatedAfter=2026-09-01T00:00:00Z&sort=updatedAt&dir=asc&limit=100&page=1" \ --header "Authorization: Bearer $SAFETY_RADAR_API_KEY"const BASE = 'https://safe-api.safetyradar.com/v2';const headers = { Authorization: `Bearer ${process.env.SAFETY_RADAR_API_KEY}` };
// Returns the new cursor to save for the next run.async function syncEvents(cursor) { let newest = cursor; for (let page = 1; ; page++) { const params = new URLSearchParams({ updatedAfter: cursor, sort: 'updatedAt', dir: 'asc', limit: '100', page: String(page), }); const res = await fetch(`${BASE}/events?${params}`, { headers }); if (!res.ok) throw new Error(`Safety Radar API returned ${res.status}`); const { data, meta } = await res.json();
for (const event of data) { await upsertEvent(event); // your code: insert or update by event.id if (event.updatedAt > newest) newest = event.updatedAt; } if (!meta.pagination.hasNext) return newest; }}import osimport requests
BASE = "https://safe-api.safetyradar.com/v2"HEADERS = {"Authorization": f"Bearer {os.environ['SAFETY_RADAR_API_KEY']}"}
def sync_events(cursor: str) -> str: """Returns the new cursor to save for the next run.""" newest = cursor page = 1 while True: res = requests.get( f"{BASE}/events", params={ "updatedAfter": cursor, "sort": "updatedAt", "dir": "asc", "limit": 100, "page": page, }, headers=HEADERS, ) res.raise_for_status() body = res.json() for event in body["data"]: upsert_event(event) # your code: insert or update by event["id"] newest = max(newest, event["updatedAt"]) if not body["meta"]["pagination"]["hasNext"]: return newest page += 1Tips for reliable syncing
Section titled “Tips for reliable syncing”- Insert or update by
id. The same record can appear in two syncs in a row, for example the last record of one sync and the first of the next. Make your processing safe to repeat. - Compare timestamps as UTC ISO strings as returned by the API. Strings in this format sort in time order.
- Metrics use UTC only. On
GET /metricsandGET /events/metrics/bulk, sendupdatedAfterandupdatedBeforein UTC with aZsuffix (for example2026-09-01T00:00:00Z), not with an offset such as+02:00. Their bounds are inclusive, so the record at your cursor is returned again. - Poll at a sensible interval. Every few minutes is plenty for most integrations and stays well within the rate limit.

