Pagination
How list endpoints page through results: limit and offset for reports, cursors for lists.
Endpoints that can return many rows page them in one of two ways:
- Ranked reports (breakdowns, top pages, countries) take
limitandoffset, and return the total inmeta.totalCount. - Lists of individual records (sessions, raw events, users, payments) take
limitandcursor, and return the next page's cursor inmeta.nextCursor.
Limit and offset
A ranked report sorts its rows by a metric, highest first, with a fixed tie-break so the order is stable, and returns one slice:
| Parameter | Default | Bounds |
|---|---|---|
limit | 50 (some endpoints differ, see below) | 1 to the endpoint's maximum |
offset | 0 | 0 to 100 000 |
# The third and fourth entry pages of the last 30 days
curl -G -H "Authorization: Bearer $PILOTFOX_API_KEY" \
"https://pilotfox.app/api/v1/sites/SITE_ID/breakdown" \
--data-urlencode 'dim=entry_page' \
--data-urlencode 'last=30d' \
--data-urlencode 'limit=2' \
--data-urlencode 'offset=2'{
"data": [
{ "value": "/blog/post-3", "visitors": 1, "sessions": 1, "pageviews": 1, "bounce_rate": 100, "avg_session_duration": 0, "percentage": 12.5 },
{ "value": "/blog/post-4", "visitors": 1, "sessions": 1, "pageviews": 1, "bounce_rate": 100, "avg_session_duration": 0, "percentage": 12.5 }
],
"meta": {
"window": { "from": "2026-09-27T20:36:28.668Z", "to": "2026-10-27T20:36:28.668Z", "tz": "Europe/Berlin", "bucket": "day" },
"totalCount": 7,
"cache": "miss"
}
}meta.totalCount is the number of rows across all pages: stop once offset + limit >= totalCount.
/vitals/breakdown and /events/autocapture do not return it; stop when a page has fewer rows than limit.
| Endpoint | limit default | limit max |
|---|---|---|
/breakdown, /pages, /vitals/breakdown, /events/autocapture | 50 | 1 000 |
/events/names, /errors | 50 | 200 |
/geo/countries, /geo/regions | 1 000 | 1 000 |
/geo/cities | 100 | 1 000 |
/revenue/breakdown, /revenue/subscriptions/breakdown | 50 | 1 000 |
/bots/breakdown | 50 | 1 000 |
/search-console/queries, /search-console/pages, /search-console/countries, /search-console/devices | 50 | 1 000 |
Paths are relative to /api/v1/sites/{siteId}.
A ranked report is recomputed on each request (or served from the short-lived cache),
so with a window that ends at "now" (last=…), rows can move between pages as new data arrives. Use fixed start
and end when you need a consistent walk through every page.
Cursors
A list sorted by time (or by the sort you choose) returns meta.nextCursor with each page. Pass it back as cursor,
with every other parameter unchanged, to get the next page. On the last page nextCursor is null.
| Parameter | Default | Bounds |
|---|---|---|
limit | 50 | 1 to the endpoint's maximum (200 for most, see below) |
cursor | none (first page) | the meta.nextCursor of the previous page, as is |
curl -G -H "Authorization: Bearer $PILOTFOX_API_KEY" \
"https://pilotfox.app/api/v1/sites/SITE_ID/sessions" \
--data-urlencode 'start=2026-10-21T00:00:00Z' \
--data-urlencode 'end=2026-10-28T00:00:00Z' \
--data-urlencode 'limit=2'{
"data": [
{ "session_id": "10753433134694004327", "started_at": "2026-10-27T20:28:59.089Z", "entry_page": "/checkout", "pageviews": 1 },
{ "session_id": "10070578087099610281", "started_at": "2026-10-27T20:23:38.097Z", "entry_page": "/blog/post-3", "pageviews": 1 }
],
"meta": {
"window": { "from": "2026-10-21T00:00:00.000Z", "to": "2026-10-28T00:00:00.000Z", "tz": "Europe/Berlin", "bucket": "day" },
"nextCursor": "WyJzdGFydGVkIiwxNzkzMTMyNjE4MDk3LCIxMDA3MDU3ODA4NzA5OTYxMDI4MSJd"
}
}The next page is the same request with --data-urlencode 'cursor=WyJzdGFydGVk…' added.
A loop that reads every page, in Node.js:
async function* allSessions(siteId, params) {
let cursor;
do {
const query = new URLSearchParams({ ...params, limit: "200", ...(cursor && { cursor }) });
const res = await fetch(`https://pilotfox.app/api/v1/sites/${siteId}/sessions?${query}`, {
headers: { Authorization: `Bearer ${process.env.PILOTFOX_API_KEY}` },
});
if (!res.ok) throw new Error(`PilotFox ${res.status}`);
const { data, meta } = await res.json();
yield* data;
cursor = meta.nextCursor;
} while (cursor);
}
for await (const session of allSessions("SITE_ID", { start: "2026-10-01T00:00:00Z", end: "2026-10-08T00:00:00Z" })) {
console.log(session.session_id);
}Rules of the cursor:
- Opaque. Pass it back exactly as received (it is URL-safe). A cursor that was altered or does not fit the list
(for example one made with another
sort) is refused with400Invalid cursor. - Keep the other parameters. The cursor marks a position in the sort order; it does not remember the window, filters or sort. Send the same ones with every page.
- Stop on
null. Some lists setnextCursorwhenever a page is full, so the last page can come back with fewer rows thanlimit, or empty, beforenextCursorturnsnull. - Stable under new data. Each page continues strictly after the last row of the previous one, so in a list sorted
by time, rows that arrive meanwhile do not shift the pages. Still, prefer fixed
startandendinstants tolast=…for a long walk: a window ending at "now" also moves its beginning forward.
| Endpoint | Sorted by | limit max |
|---|---|---|
/sessions | sort, highest first (default started: newest first) | 200 |
/sessions/{sessionId} (the session's events) | time, oldest first | 500 |
/events (raw events) | time, newest first | 500 |
/errors/{fingerprint}/occurrences | time, newest first | 200 |
/users | sort, highest first (default last_seen: most recent first) | 200 |
/users/{userId}/sessions | sort, highest first (default started) | 200 |
/goals/{id}/conversions | conversion time, newest first | 200 |
/funnels/{id}/sessions | start time, newest first | 200 |
/journeys/sessions | start time, newest first | 200 |
/replays | start time, newest first | 200 |
/payments | time, newest first | 200 |
Paths are relative to /api/v1/sites/{siteId}. /goals/{id}/conversions also returns meta.totalCount, the number
of conversions across all pages. The instance administration lists (/api/v1/admin/orgs,
/admin/sites, /admin/backups/runs) page the same way.
The realtime tail of raw events
GET /sites/{siteId}/events also has a tail mode for live views: instead of
a window and a cursor, send since, an ISO 8601 instant within the last 24 hours. The answer holds the events from
since to now, newest first, up to 500 whatever the limit, and nextCursor is null. To poll, send the newest ts
you received as the next since, and skip the events you already have (the window includes since itself).
Other limits
Some reports take a limit but no offset or cursor, because they return one bounded set:
| Endpoint | limit default | limit max |
|---|---|---|
/journeys (the most common paths) | 100 | 500 |
/geo/points | 20 000 | 20 000 |
/geo/timeline | 100 000 | 100 000 |
/traits/keys, /traits/values | 50 | 1 000 |
/bots/timeseries (series per top key) | 5 | 20 |
/search-console/insights | 50 | 200 |
Session replay chunks (/replays/{sessionId}/chunks)
page by sequence number: pass meta.nextCursor back as from_seq.