Pagination
The Fleeta Open API uses two pagination styles side by side, chosen by the nature of the data (hybrid pagination).
High-volume, time-series data (events, GPS telemetry, audit logs) uses cursors. Iterating a list that grows in real time with offsets causes duplicates and gaps, because new items are inserted at the front between page fetches. A cursor resumes from "right after the last item seen," so this problem does not occur.
Small, static data (devices, geofences, webhook subscriptions) uses offsets. These records are not created every second, so the duplication risk is effectively nil, and offsets suit list UIs that need total counts and arbitrary page jumps.
Style by endpoint
| Style | Endpoints |
|---|---|
| cursor | GET /v1/events · GET /v1/events/feed · GET /v1/fleet/locations · GET /v1/fleet/locations/feed · GET /v1/devices/{psn}/trips · GET /v1/gps/export-jobs · GET /v1/geofence-alerts · GET /v1/reports/driving · GET /v1/audit-logs · GET /v1/api-logs |
| offset | GET /v1/devices · GET /v1/geofences · GET /v1/webhooks · GET /v1/webhooks/{webhookId}/deliveries · GET /v1/devices/{psn}/sd-files · GET /v1/media/recall-jobs |
Endpoints not listed above are not paginated: they return the full result set
in one response, with no pagination object. GET /v1/groups is one such
endpoint.
Cursor style
The request parameters are after (the nextCursor from the previous
response; omit for the first page) and limit (default 50, max 200 — except
GET /v1/fleet/locations/feed, which defaults to 100 with a max of 500).
limit is an upper bound, not a promise. Endpoints that merge several
source records into one row — GET /v1/events and GET /v1/events/feed merge
the files of one recording — can return fewer items than you asked for, and
occasionally none at all, while hasMore is still true.
curl "https://openapi.fleeta.io/v1/events?from=2026-07-01T00:00:00Z&limit=100" \
-H "Authorization: Bearer flt_live_..."
{
"data": [ { "eventId": "evt_0032", "type": "harsh_braking", "...": "..." } ],
"pagination": {
"nextCursor": "eyJsYXN0SWQiOiJldnRfMDAzMiJ9",
"hasMore": true
}
}
Pass nextCursor as the after parameter of the next request to iterate;
when nextCursor is null (hasMore: false), you are on the last page.
Never stop because data came back shorter than limit or empty — that only
means the scan ended on an item boundary. The one condition that ends the walk
is hasMore: false.
let after;
do {
const url = new URL('https://openapi.fleeta.io/v1/events');
url.searchParams.set('from', '2026-07-01T00:00:00Z');
url.searchParams.set('limit', '100');
if (after) url.searchParams.set('after', after);
const res = await fetch(url, { headers: { Authorization: `Bearer ${API_KEY}` } });
const { data, pagination } = await res.json();
for (const ev of data) handle(ev);
after = pagination.nextCursor;
} while (after);
A cursor is a string that encodes internal server state. Do not decode it,
depend on its contents, or construct one yourself — the internal format may
change without notice. A corrupted cursor, or one issued by a different
endpoint (each endpoint only accepts the cursors it issued), is rejected
with 400 invalid_cursor rather than silently restarting from the first
page. Always pass the response's nextCursor unchanged, and only to the
endpoint that returned it.
feed endpoints — cursor tail polling
GET /v1/events/feed and GET /v1/fleet/locations/feed are dedicated
tail polling endpoints that use the cursor as a bookmark for "the last
point received." Unlike regular lists:
- They return only items that occurred after
after, in ascending time order. - They always return a
nextCursor, even when there are no new items. Store it and pass it unchanged asafteron the next poll. - The cursor is a keyset over the server-side sort key (update time + record id as a tie-break), so items sharing the same timestamp are never skipped or duplicated — an interrupted consumer resumes exactly where it left off.
This is the canonical mechanism for continuous, high-volume data such as
live fleet locations: poll GET /v1/fleet/locations/feed at intervals of
30 seconds or more, and call again with the returned nextCursor each
time. For discrete events (safety events, geofence enter/exit, export
completion), subscribe to Webhooks push instead.
start=latestA cursor-less first call to GET /v1/events/feed starts at the oldest
event — a full backlog walk, which is what you want for an export but not for
a "live" panel. Call once with start=latest: it returns no rows and a
nextCursor positioned at the newest event, so every later poll delivers
only what happened after you connected.
The API deliberately does not offer a WebSocket/streaming interface. Continuous data is served by cursor tail polling (resumable, no connection state to manage) and discrete events by webhooks — together they cover the real-time use cases with far simpler failure semantics.
let cursor = null;
async function poll() {
const url = new URL('https://openapi.fleeta.io/v1/events/feed');
if (cursor) url.searchParams.set('after', cursor);
else url.searchParams.set('start', 'latest'); // live panel: first call takes the tail position, no backlog
const res = await fetch(url, { headers: { Authorization: `Bearer ${API_KEY}` } });
const { data, pagination } = await res.json();
for (const ev of data) handle(ev); // new events only, ascending order
cursor = pagination.nextCursor; // always update and store, even on empty responses
}
setInterval(poll, 30_000); // recommended polling interval: 30 seconds or more
If you persist the cursor (file/DB), a restarted process can resume without gaps. If you need lower latency, consider a Webhooks push subscription.
Offset style
The request parameters are page (default 1) and perPage (default 20,
max 100 — except the two SD-card endpoints GET /v1/devices/{psn}/sd-files
and GET /v1/media/recall-jobs, which default to 50 with a max of 200).
curl "https://openapi.fleeta.io/v1/devices?page=2&perPage=50" \
-H "Authorization: Bearer flt_live_..."
{
"data": [ { "psn": "7XBPK0BE00000003", "name": "Van-01", "...": "..." } ],
"pagination": {
"page": 2,
"perPage": 50,
"total": 137,
"totalPages": 3
}
}
total / totalPages let you display the total count and the last page,
and the page value gives direct access to any page.
Caveats / best practices
- List responses are always
data+pagination; only the fields insidepaginationdiffer by style (cursor:nextCursor/hasMore, offset:page/perPage/total/totalPages). - The cursor style is strictly forward-only — if you need page-number jumps, first check whether the resource uses offset.
- Cursor lists have no server-side result cap — they iterate the full
history through a native keyset cursor, so even organizations with very
large event / audit-log / geofence-alert volumes never lose older records.
Page forward with
afteruntilhasMoreisfalse. - Exceeding the
limit/perPagemaximum returns400 invalid_parameter. For bulk reads, use the maximum value plus iteration. - The official SDK's
iterate()helper auto-iterates both styles — see SDKs.
Related links
- SDKs — automatic iteration with
iterate() - Webhooks — push delivery instead of polling
- Rate Limits — polling intervals and quotas