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.