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.
Cursor-based pagination (DynamoDB)
Section titled “Cursor-based pagination (DynamoDB)”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
lastKeyback 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:
- Offset mode — use
fromandsizefor page-number navigation (UI). - syncToken mode — use the returned
syncTokenfor 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:truewhen a full page was returned (more pages likely),falseon the last page.syncToken: cursor for the next page;nullon the last page. It appears at both the root level and insidepaginationfor convenience.totalis capped at 10,000 for performance. When more than 10,000 results match,totalis10000andtotalIsLowerBoundistrue(“at least 10,000”); otherwisetotalIsLowerBoundisfalseandtotalis exact.
Brand search uses the same offset envelope but does not return
syncToken,hasMore, ortotalIsLowerBound.
Characteristics
- ✅ Total count (subject to the 10,000 cap above).
- ✅ Random access via
from. - ✅ Deep pagination via
syncTokenbeyond 10,000 results. - ✅ Clear stop signal:
syncToken: nullandhasMore: false. - ⚠️
frommode is slower for deep pagination and may see duplicates/gaps if data changes mid-pagination.
When to use each
Section titled “When to use each”| Scenario | Use | Reason |
|---|---|---|
| Loading all products | syncToken (/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” button | syncToken (/search) | Simple, efficient incremental loading |
| Traditional pagination UI | Offset (/search with from) | Need page numbers (1, 2, 3…) |
| Data export/sync | syncToken (/search) | No duplicates, clear stop signal |
| WooCommerce import | syncToken (/search) | Works with WPGetAPI pagination filters |
Implementation examples
Section titled “Implementation examples”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 };}