# Pagination

Endpoints that return a list send it one page at a time. The list endpoints don't all use the same convention: they differ in parameter names, defaults, maximum page size, how they treat out-of-range values, and where the page metadata sits in the response. This guide lays those differences out so you can page through each one correctly.

## At a glance

| Endpoint | Style | Parameters | Default size | Max size | Out of range | Page metadata |
| --- | --- | --- | --- | --- | --- | --- |
| `GET /v1/datasets` | Page | `page`, `limit` | 10 | 200 | Rejected with `422` | `pagination: { page, limit, total, hasMore }` |
| `GET /v1/datasets/{datasetId}/items` | Page | `page`, `limit` | 10 | 200 | Rejected with `422` | `pagination: { page, limit, total, hasMore }` |
| `GET /v1/datasets/{datasetId}/items/queue` | Page | `page`, `limit` | 10 | 200 | Rejected with `422` | `pagination: { page, limit, total, hasMore }` |
| `GET /v1/datasets/{datasetId}/sources` | Page | `page`, `limit` | 20 | 100 | Clamped | Top level: `total, page, limit` |
| `GET /v1/datasets/{datasetId}/sources/{sourceId}/logs` | Page | `page`, `limit` | 10 | 50 | Clamped | Top level: `total, page, limit` |
| `GET /v1/datasets/{datasetId}/sources/{sourceId}/sync/jobs` | First page only | `limit` | 20 | 100 | Rejected with `422` | `pagination: { page, limit, total, totalPages }` |
| `GET /v1/chat/list` | Offset | `skip`, `limit` | 20 | 100 | Clamped | `pagination: { total, limit, skip, hasMore }` |

Pages are 1-based everywhere `page` is used. Every list is sorted newest first by default.

## Validated lists: datasets, items and queue

Datasets, dataset items and queue entries validate their parameters strictly.

- `page` must be an integer of at least 1. `limit` must be an integer from 1 to 200.
- A value that isn't a number, such as `?page=abc`, is rejected with `422`. So is `0`, a negative number, or a `limit` above 200. Nothing is clamped.
- A decimal is truncated before it is checked, so `?limit=2.9` is treated as `2`.
- The response has `pagination.hasMore`. There is no page count, so keep going while `hasMore` is `true`.

```json
{
  "items": [ ... ],
  "pagination": { "page": 1, "limit": 10, "total": 1284, "hasMore": true }
}
```

The item list returns only `active` items. The queue list returns entries in every status except `archived`.

## Clamped lists: data sources and sync logs

Data sources and sync logs never reject paging values. They adjust them instead.

- A `page` below 1, or one that isn't a number, becomes `1`.
- A `limit` above the maximum becomes the maximum: 100 for sources, 50 for logs.
- A `limit` of `0`, or one that isn't a number, becomes the default: 20 for sources, 10 for logs.
- A negative `limit` becomes `1`.
- The page metadata sits at the top level of the response next to `success`. There is no `hasMore`, so compute it from `page * limit < total`.

```json
{
  "success": true,
  "sources": [ ... ],
  "total": 42,
  "page": 1,
  "limit": 20
}
```

Always read `limit` back from the response rather than assuming your requested value was used.

## Sync jobs: first page only

The sync job list returns only the most recent jobs.

- `limit` must be an integer from 1 to 100. Values outside that range, or that aren't a number, are rejected with `422`.
- There is no `page` or `offset` parameter. Every request returns the newest `limit` jobs, and `pagination.page` is always `1`.
- The metadata includes `totalPages` rather than `hasMore`.

To see more jobs, raise `limit` up to 100. Finished jobs are deleted seven days after they complete, so for longer history page through sync logs instead.

## Chat list: offset-based

The chat list uses `skip` and `limit` instead of `page`.

- `skip` is the number of chats to pass over. A negative value, or one that isn't a number, becomes `0`.
- `limit` above 100 becomes 100. A `limit` of `0`, or one that isn't a number, becomes the default of 20. A negative `limit` becomes `1`.
- `pagination.hasMore` is `true` while `skip + limit` is less than `total`.
- `sortBy` can be `updatedAt` (default), `createdAt` or `chatId`. `sortOrder` can be `asc` or `desc` (default).

```json
{
  "chats": [ ... ],
  "pagination": { "total": 137, "limit": 20, "skip": 0, "hasMore": true }
}
```

The default sort is by `updatedAt`, which changes as conversations continue. If chats are active while you page, a chat can move between pages. Sort by `createdAt` for a stable walk.

## Endpoints without pagination

Search returns a single ranked list. Set its size with `limit` (1 to 100, rejected outside that range); there is no second page. Detect datasets and available genders return everything in one response.

## Iterating over every page

This JavaScript helper handles all three styles. Tell it which style an endpoint uses and where its records are:

```javascript
const BASE_URL = "https://stylor.ai/api";

async function getJson(path, params) {
  const url = new URL(BASE_URL + path);
  for (const [key, value] of Object.entries(params)) url.searchParams.set(key, String(value));

  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.STYLOR_API_KEY}` },
  });
  const body = await res.json();
  if (!res.ok) throw new Error(`${res.status}: ${body.error}`);
  return body;
}

async function* listAll(path, { recordsKey, style = "page", limit = 100 }) {
  let page = 1;
  let skip = 0;

  while (true) {
    const params = style === "offset" ? { skip, limit } : { page, limit };
    const body = await getJson(path, params);
    const records = body[recordsKey] ?? [];
    yield* records;

    // Metadata is nested under `pagination` on some endpoints and top level on others.
    const meta = body.pagination ?? body;
    const appliedLimit = meta.limit ?? limit;

    let hasMore;
    if (typeof meta.hasMore === "boolean") {
      hasMore = meta.hasMore;
    } else if (style === "offset") {
      hasMore = skip + appliedLimit < meta.total;
    } else {
      hasMore = page * appliedLimit < meta.total;
    }

    if (!hasMore || records.length === 0) return;
    page += 1;
    skip += appliedLimit;
  }
}

// Validated list: limit up to 200.
for await (const item of listAll(`/v1/datasets/${datasetId}/items`, { recordsKey: "items", limit: 200 })) {
  console.log(item.product.name);
}

// Clamped list: the response reports the limit that was applied.
for await (const log of listAll(`/v1/datasets/${datasetId}/sources/${sourceId}/logs`, { recordsKey: "logs", limit: 50 })) {
  console.log(log.logId, log.status);
}

// Offset list.
for await (const chat of listAll("/v1/chat/list", { recordsKey: "chats", style: "offset", limit: 100 })) {
  console.log(chat.chatId);
}
```

The same approach in Python:

```python
import os
import requests

BASE_URL = "https://stylor.ai/api"


def list_all(path, records_key, style="page", limit=100):
    page, skip = 1, 0
    headers = {"Authorization": f"Bearer {os.environ['STYLOR_API_KEY']}"}

    while True:
        params = {"skip": skip, "limit": limit} if style == "offset" else {"page": page, "limit": limit}
        res = requests.get(f"{BASE_URL}{path}", headers=headers, params=params, timeout=60)
        body = res.json()
        if not res.ok:
            raise RuntimeError(f"{res.status_code}: {body.get('error')}")

        records = body.get(records_key) or []
        yield from records

        meta = body.get("pagination") or body
        applied_limit = meta.get("limit", limit)
        if isinstance(meta.get("hasMore"), bool):
            has_more = meta["hasMore"]
        elif style == "offset":
            has_more = skip + applied_limit < meta["total"]
        else:
            has_more = page * applied_limit < meta["total"]

        if not has_more or not records:
            return
        page += 1
        skip += applied_limit


for source in list_all(f"/v1/datasets/{dataset_id}/sources", "sources", limit=100):
    print(source["sourceId"], source["status"])
```

Don't use these helpers for sync jobs. That endpoint only ever returns its first page, so a single request with `limit=100` is all you can get.

## Tips

- Every page is a separate metered request. Use the largest page size an endpoint allows when you need everything.
- Offset and page pagination aren't snapshots. Records created or deleted while you iterate can shift items between pages, so de-duplicate by id if that matters.
- For validated lists, keep `limit` within range. A value one over the maximum fails the whole request rather than being reduced.
