Skip to content

Pagination and polling

List endpoints on the OLET API take a limit, a cursor, and an order, and return a next_cursor and a has_more flag. Store the last next_cursor you saw and pass it back as cursor on your next call, and you have a working polling loop.

Call the endpoint with a limit (1 to 200, default 50). The response carries the page of data plus next_cursor and has_more.

Terminal window
curl "https://api.oletapp.com/your-store/v1/products?limit=50" \
-H "Authorization: Bearer olet_live_YOUR_API_KEY"
let cursor
let hasMore = true
while (hasMore) {
const url = new URL('https://api.oletapp.com/your-store/v1/products')
url.searchParams.set('limit', '50')
if (cursor) url.searchParams.set('cursor', cursor)
const page = await (await fetch(url, { headers: { Authorization: 'Bearer olet_live_YOUR_API_KEY' } })).json()
// ...handle page.data here...
cursor = page.next_cursor
hasMore = page.has_more
}

Rows come back oldest change first. A cursor is opaque: treat it as an unreadable string and pass it back byte for byte, exactly as you received it. Never construct one yourself.

order=desc returns the newest changes first, but only as a single first page: it never returns a next_cursor, has_more is always false, and any cursor you send is ignored apart from being checked. Use it to peek at the latest changes, not to walk a list.

Store the last non-null next_cursor you received, and pass it back as cursor to get everything that changed since. A page with no rows still returns next_cursor: null, which means “keep using the cursor you already have”, not “start over”.

Every row carries a version that moves whenever anything in that row changes. On a product, that includes its codes, tags, suppliers, variant details and components, and even a rename of an attached tag or supplier, so a product reappears in your poll when any of those change. Deleting a brand, category, tag or supplier re-versions every product that used it, so expect a burst of products on your next poll.

Add include_deleted=true to also receive rows that were deleted since your cursor. On GET /products and GET /customers a deleted row comes back as a tombstone, {"id": "...", "deleted_at": "...", "version": "..."}, so your local copy can remove it. The other lists (brands, categories, tags, suppliers and the rest) return deleted rows in their normal shape with no deleted marker, so to find removals there, list again and compare ids. include_deleted=true only works with order=asc, the default, and combining it with order=desc answers 400 VALIDATION_FAILED. GET /sales refuses include_deleted altogether, because sales are never deleted.

GET /inventory/adjustment-reasons and the stock levels of one product are not paginated: they return every row, with no cursor.

A cursor the server cannot read answers 400 VALIDATION_FAILED rather than silently restarting you at the first page. If you see this, discard your stored cursor and start a fresh sync from the beginning rather than retrying the same value.

See idempotency for how to safely retry a write, or the full API reference for every list endpoint’s fields.