PilotFoxDocs

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 limit and offset, and return the total in meta.totalCount.
  • Lists of individual records (sessions, raw events, users, payments) take limit and cursor, and return the next page's cursor in meta.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:

ParameterDefaultBounds
limit50 (some endpoints differ, see below)1 to the endpoint's maximum
offset00 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'
Response
{
  "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.

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.

ParameterDefaultBounds
limit501 to the endpoint's maximum (200 for most, see below)
cursornone (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'
Response (each session abridged)
{
  "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:

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 with 400 Invalid 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 set nextCursor whenever a page is full, so the last page can come back with fewer rows than limit, or empty, before nextCursor turns null.
  • 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 start and end instants to last=… for a long walk: a window ending at "now" also moves its beginning forward.
EndpointSorted bylimit max
/sessionssort, highest first (default started: newest first)200
/sessions/{sessionId} (the session's events)time, oldest first500
/events (raw events)time, newest first500
/errors/{fingerprint}/occurrencestime, newest first200
/userssort, highest first (default last_seen: most recent first)200
/users/{userId}/sessionssort, highest first (default started)200
/goals/{id}/conversionsconversion time, newest first200
/funnels/{id}/sessionsstart time, newest first200
/journeys/sessionsstart time, newest first200
/replaysstart time, newest first200
/paymentstime, newest first200

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:

Endpointlimit defaultlimit max
/journeys (the most common paths)100500
/geo/points20 00020 000
/geo/timeline100 000100 000
/traits/keys, /traits/values501 000
/bots/timeseries (series per top key)520
/search-console/insights50200

Session replay chunks (/replays/{sessionId}/chunks) page by sequence number: pass meta.nextCursor back as from_seq.

On this page