# Pagination and polling

How list endpoints page through results and how to poll them for changes over time.

Source: https://getolet.com/resources/developers/pagination-and-polling/
Updated: 2026-10-15

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.

## How do I page through a list

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

```bash
curl "https://api.oletapp.com/your-store/v1/products?limit=50" \
  -H "Authorization: Bearer olet_live_YOUR_API_KEY"
```

```js
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.

## How do I poll for changes

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.

## What happens with a bad 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.

## Where to go next

See [idempotency](/resources/developers/idempotency/) for how to safely retry a write, or the full [API reference](/resources/developers/reference/) for every list endpoint's fields.
