Skip to main content

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

StyleEndpoints
cursorGET /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
offsetGET /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.

A short page is not the end

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);
Cursors are opaque tokens

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 as after on 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.

Starting a live panel? Begin with start=latest

A 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.

No WebSocket — by design

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 inside pagination differ 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 after until hasMore is false.
  • Exceeding the limit/perPage maximum returns 400 invalid_parameter. For bulk reads, use the maximum value plus iteration.
  • The official SDK's iterate() helper auto-iterates both styles — see SDKs.
  • SDKs — automatic iteration with iterate()
  • Webhooks — push delivery instead of polling
  • Rate Limits — polling intervals and quotas