> ## Documentation Index
> Fetch the complete documentation index at: https://cactal.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Pagination

> Cactal list endpoints use cursor pagination: pass cursor and limit, read items and nextCursor. Defaults, maxima, and iteration patterns.

List endpoints that can grow without bound use keyset (cursor) pagination. You page forward by handing each response's cursor back to the next request.

## Request parameters

<ParamField query="cursor" type="string">
  Opaque position token from a previous response's `nextCursor`. Omit it on the first request. Never construct or modify cursors.
</ParamField>

<ParamField query="limit" type="integer" default="20">
  Items per page. Minimum `1`, maximum `100` on public list endpoints. Most lists default to `20`; the assets list defaults to `50`.
</ParamField>

## Response envelope

```json theme={null}
{
  "items": [
    { "id": "V1StGXR8Z5jdHi6BmyTxQ", "name": "Acme Plumbing" }
  ],
  "nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTA3LTA4In0"
}
```

* `items` — the page of results, in the endpoint's documented order.
* `nextCursor` — pass as `cursor` to fetch the next page. `null` means you have the last page.

## Iterating all pages

<CodeGroup>
  ```bash iterate.sh theme={null}
  cursor=""
  while :; do
    response=$(curl -s "https://api.cactal.ai/v1/websites?limit=100${cursor:+&cursor=$cursor}" \
      -H "Authorization: Bearer $CACTAL_API_KEY")
    echo "$response" | jq -r '.items[].name'
    cursor=$(echo "$response" | jq -r '.nextCursor // empty')
    [ -z "$cursor" ] && break
  done
  ```

  ```typescript iterate.ts theme={null}
  const baseUrl = 'https://api.cactal.ai/v1'
  const headers = { Authorization: `Bearer ${process.env.CACTAL_API_KEY}` }

  let cursor: string | null = null
  do {
    const params = new URLSearchParams({ limit: '100' })
    if (cursor) params.set('cursor', cursor)
    const response = await fetch(`${baseUrl}/websites?${params}`, { headers })
    const page = await response.json()
    for (const website of page.items) console.log(website.name)
    cursor = page.nextCursor
  } while (cursor)
  ```
</CodeGroup>

## Rules and edge cases

* Cursors are tied to the endpoint and its sort mode. Changing `sort` or filters invalidates a cursor — start over without one.
* An invalid or corrupted cursor returns `400` `validation`. Restart from the first page.
* Pages are stable against inserts and deletes: keyset pagination never skips or duplicates items because of concurrent writes elsewhere in the list.
* Treat `nextCursor: null` as the only end-of-list signal. A short page is not one — a page can legally hold fewer than `limit` items.

## Endpoints that differ

* **CMS items** (`GET /v1/cms/collections/{collectionId}/items`) additionally supports 1-based `page` offsets as an alternative to `cursor` (the two are mutually exclusive) and returns `{ items, nextCursor, nextPage }`. It also accepts `filter`, `search`, `sort`, and `depth` — see the endpoint page.
* **Fixed-cardinality reads** return plain objects or arrays with no envelope, because a product rule caps how many results can exist: website domains (one platform domain plus your custom-domain quota), CMS collection fields (a per-collection field limit), notification preferences (a fixed category catalog), and analytics (explicit top-N and max-points limits, with truncation reported in the response).
