Skip to content

Pagination

Outcome
Traverse large collections without duplicate or skipped pages.
Prerequisites
A list endpoint and stable query/order parameters.
You'll use
limit, next_cursor, prev_cursor, and pagination metadata.

All list endpoints return paginated results using cursor-based pagination. This approach is more efficient than offset-based pagination, especially for large datasets.

How It Works

  1. Request a page with an optional limit
  2. The response includes a pagination object with cursors
  3. Pass next_cursor to get the next page, prev_cursor to go back

Results are ordered by the endpoint’s default order unless you override it with order_by.

Cursors are opaque tokens. Treat them as read-only values from the API. Do not parse them, construct them manually, or reuse them with a different order_by value. Changing ordering means restarting pagination from the first page.

Query Parameters

ParameterTypeDefaultDescription
limitnumber10Items per page (1–100)
next_cursorstring—Opaque cursor for the next page
prev_cursorstring—Opaque cursor for the previous page
include_total_countbooleantrueInclude total count in response

Response Format

Every list endpoint returns this structure:

{
"data": [ ... ],
"pagination": {
"total": 84,
"next_cursor": "inv_def456",
"prev_cursor": null,
"has_more": true
}
}
FieldDescription
totalTotal number of matching items (or -1 if include_total_count=false)
next_cursorPass this as next_cursor to get the next page. null if no more results. Valid only for the same effective ordering.
prev_cursorPass this as prev_cursor to go back. null on the first page. Valid only for the same effective ordering.
has_moretrue if there are more results after this page

Walking Through Pages

To iterate through all results, keep requesting pages until has_more is false:

Basic sdktypescript
// title: SDK
// First page
const page1 = await sdk.invoices.list({ limit: 20 });

console.log(page1.data); // Invoice[]
console.log(page1.pagination.has_more); // true

// Next page using cursor
if (page1.pagination.next_cursor) {
  const page2 = await sdk.invoices.list({
    limit: 20,
    next_cursor: page1.pagination.next_cursor,
  });
}
Basic curlbash
# title: cURL
curl "https://eu.spaceinvoices.com/invoices?limit=20" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "X-Entity-Id: ent_123"

# Next page using cursor from response
curl "https://eu.spaceinvoices.com/invoices?limit=20&next_cursor=CURSOR_FROM_RESPONSE" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "X-Entity-Id: ent_123"

Fetching Every Page Automatically

Hand-rolling a while (has_more) loop is easy to get wrong — a typo in the cursor parameter, or forgetting to update it, means the loop never advances and keeps re-requesting the same page. That exact mistake has taken down a production database by looping for 18+ hours.

The JavaScript SDK exports a listAll() helper from the main @spaceinvoices/js-sdk package (it is not available from the lean @spaceinvoices/js-sdk/sdk subpath). It is a standalone function, not a method on the list call itself: pass it any list method plus the options you’d normally pass to that method, and it walks every page for you, yielding one item at a time:

List all sdkjavascript
// title: Fetch every page automatically
// listAll() is exported from the main __PROTECTED_0__ package (the lean
// __PROTECTED_1__ subpath does not include it). It walks every page of a
// list method for you and stops as soon as the API reports no further pages
// (pagination.next_cursor is null) - no manual cursor loop.
for await (const invoice of listAll(sdk.invoices.list, { limit: 100 })) {
  console.log(invoice.id);
}

// listAll() carries a safety limit (1000 pages) and also throws if the API ever
// returns a non-advancing cursor, or a response with no pagination envelope at all
// (some list endpoints return a bare array and are not cursor-paginated). A
// non-advancing cursor is the exact bug that once sent a customer's hand-rolled
// loop into an 18-hour, half-million-request outage - narrow your filters (query,
// date range, etc.) if you expect a result set large enough to need more than 1000
// pages.

listAll() works with endpoints that return the standard { data, pagination: { next_cursor } } envelope. For the few list methods that return something else, such as a plain array, call them directly.

It throws a clear error instead of returning partial results when:

  • the response isn’t in the standard shape;
  • the cursor stops advancing;
  • it reaches 1000 pages, which almost always means a pagination bug rather than a huge result set.

Performance Tip

The total count requires an extra database query. For large datasets where you don’t need the exact count, disable it:

?include_total_count=false

When disabled, pagination.total returns -1 but pagination still works normally via has_more and cursors.