Skip to content

Pagination

The CRM API uses two different pagination strategies depending on the endpoint’s data source. This page explains both and when to reach for each. For per-endpoint parameters, see the API Reference.

Used by: GET /products — deprecated: use GET /products/search with syncToken for new integrations and catalog sync (see the product sync guide).

  • Returns a lastKey (base64-encoded cursor) with each response.
  • Pass the lastKey back to get the next page.
  • Efficient for large datasets; consistent results even with concurrent writes.
{
"products": [],
"lastKey": "eyJpZCI6IjEyMyIsImNyZWF0ZWRBdCI6IjIwMjUtMDEtMDEifQ=="
}

Characteristics

  • ✅ Constant-time performance regardless of page depth.
  • ✅ No duplicate/missing items even with concurrent writes.
  • ✅ Stateless — each request is independent.
  • ❌ No random access (can’t jump to “page 5”).
  • ❌ No total count.

Offset + syncToken pagination (OpenSearch)

Section titled “Offset + syncToken pagination (OpenSearch)”

Used by: GET /products/search (and /brands/search for offset only).

Product search supports two modes:

  1. Offset mode — use from and size for page-number navigation (UI).
  2. syncToken mode — use the returned syncToken for deep/sequential pagination (sync, export, WooCommerce).
{
"data": [],
"pagination": {
"total": 156,
"totalIsLowerBound": false,
"from": 0,
"size": 10,
"hasMore": true,
"syncToken": "WzE3NTYyOTA5NjAzMDks..."
},
"syncToken": "WzE3NTYyOTA5NjAzMDks...",
"meta": { "queryTimeMs": 23, "source": "opensearch" }
}
  • hasMore: true when a full page was returned (more pages likely), false on the last page.
  • syncToken: cursor for the next page; null on the last page. It appears at both the root level and inside pagination for convenience.
  • total is capped at 10,000 for performance. When more than 10,000 results match, total is 10000 and totalIsLowerBound is true (“at least 10,000”); otherwise totalIsLowerBound is false and total is exact.

Brand search uses the same offset envelope but does not return syncToken, hasMore, or totalIsLowerBound.

Characteristics

  • ✅ Total count (subject to the 10,000 cap above).
  • ✅ Random access via from.
  • ✅ Deep pagination via syncToken beyond 10,000 results.
  • ✅ Clear stop signal: syncToken: null and hasMore: false.
  • ⚠️ from mode is slower for deep pagination and may see duplicates/gaps if data changes mid-pagination.
ScenarioUseReason
Loading all productssyncToken (/products/search)Efficient, consistent for large datasets (/products is deprecated for sync)
User searching (UI)Offset (/search with from)Users expect page numbers and totals
”Load more” buttonsyncToken (/search)Simple, efficient incremental loading
Traditional pagination UIOffset (/search with from)Need page numbers (1, 2, 3…)
Data export/syncsyncToken (/search)No duplicates, clear stop signal
WooCommerce importsyncToken (/search)Works with WPGetAPI pagination filters

Cursor-based (GET /products — deprecated; prefer the syncToken example below)

async function* fetchAllProducts(limit = 100) {
let lastKey = null;
do {
const params = new URLSearchParams({ limit: String(limit) });
if (lastKey) params.set('lastKey', lastKey);
const response = await fetch(`https://{api-base-url}/products?${params}`, {
headers: { Authorization: `Bearer ${token}` }
});
const data = await response.json();
yield data.products;
lastKey = data.lastKey;
} while (lastKey);
}

Offset-based (GET /products/search)

async function searchWithPagination(query, page = 1, pageSize = 20) {
const from = (page - 1) * pageSize;
const params = new URLSearchParams({ q: query, size: String(pageSize), from: String(from) });
const response = await fetch(`https://{api-base-url}/products/search?${params}`, {
headers: { Authorization: `Bearer ${token}` }
});
const data = await response.json();
return {
results: data.data,
totalResults: data.pagination.total,
totalPages: Math.ceil(data.pagination.total / pageSize),
currentPage: page,
hasNextPage: (from + pageSize) < data.pagination.total
};
}