Guides

Pagination

Cursor-based pagination — opaque, signed cursors that freeze the request they were issued for.

A search that matches thousands of rows hands them over a page at a time. The cursor is your bookmark: pass it back and the next page picks up exactly where the last one ended, even if the index changed in between.

These routes paginate with cursors:

On the cursor routes there is no page or offset parameter — not accepted, not deprecated, never present. Sending one is a 422.

A few routes page differently. The filter-values routes take a page number, GET /v1/feedback takes an offset, and GET /v1/agents/runs takes a limit only. The nine /v1/entities/* routes and POST /v1/relationships/institutions always return a single page.

Every paginated response carries two fields that always agree:

  • next_cursor — an opaque string, or null when there is no further page.
  • has_more — true exactly when next_cursor is not null. This invariant holds on every response, so you can loop on either.

Use cases#

Collecting a full segment

A filter search defines an audience and you need every row, not the first page. Loop on has_more, resending the same body plus the cursor. You get every matching row up to the 10,000-result depth limit, in a stable order. The search is charged for the results it delivers across all its pages, so page size does not change the total, and sending a cursor you already used again costs nothing.

Paging a results UI

A user scrolls or clicks "next". Keep the original request body and the latest next_cursor in page state and fetch on demand. Pages are consistent and non-overlapping; if the user changes any filter, discarding the cursor and starting from page 1 is the designed behaviour, not a workaround.

The walk#

Resend the same request, plus the cursor from the previous page:

# Page 1
curl -X POST https://api.sproutsdata.ai/v1/search \
  -H "X-API-Key: $SPROUTS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "software companies in Germany", "mode": "auto", "limit": 10}'
# → { ..., "has_more": true, "next_cursor": "eyJmIjoiMDNjMjUzNzk3MjM3MTI3ZSIsIm8iOjEs..." }

# Page 2 — the SAME body, plus cursor
curl -X POST https://api.sproutsdata.ai/v1/search \
  -H "X-API-Key: $SPROUTS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "software companies in Germany", "mode": "auto", "limit": 10,
       "cursor": "eyJmIjoiMDNjMjUzNzk3MjM3MTI3ZSIsIm8iOjEs..."}'

Cursors are opaque and frozen to the request#

Cursors are signed server-side. Treat them as opaque strings: don't decode, edit, or construct them. A tampered cursor fails verification with 422 pagination.cursor_invalid.

More importantly, a cursor is a bookmark into one specific search: it carries a fingerprint of the request that produced it — every field except cursor and limit. Changing anything else mid-walk — the query, a filter, the mode, the category, the sort clause — is a clean error rather than a silently reshuffled result set:

{
  "status": 422,
  "code": "pagination.cursor_invalid",
  "title": "Cursor does not match this request",
  "detail": "The request changed between pages. A cursor is only valid for the exact search that produced it."
}

Expiry#

Cursors expire 24 hours after issue:

{
  "status": 422,
  "code": "pagination.cursor_expired",
  "title": "Cursor expired",
  "detail": "This cursor is too old to be trusted. Run the search again."
}

The expiry exists so a cursor cannot be replayed against an index that has moved underneath it and quietly return a mixture of two points in time. Recovery is always the same: re-run the search from page 1.

Depth limit#

You cannot page beyond 10,000 results:

{
  "status": 422,
  "code": "pagination.depth_exceeded",
  "title": "Pagination too deep",
  "detail": "Cannot page beyond 10000 results. Narrow the search instead."
}

Deep pagination is how a bulk export disguises itself as browsing. If you need more than 10,000 rows of a segment, narrow the search into slices (by industry, geography, size band) and walk each slice.

has_more: false is not always "you have everything"#

The upstream engine can decline to issue a further cursor while more matches exist. When that happens, the response says so explicitly: degradation[] contains a pagination_truncated entry alongside has_more: false.