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.
pagemust be an integer of at least 1.limitmust be an integer from 1 to 200.- A value that isn't a number, such as
?page=abc, is rejected with422. So is0, a negative number, or alimitabove 200. Nothing is clamped. - A decimal is truncated before it is checked, so
?limit=2.9is treated as2. - The response has
pagination.hasMore. There is no page count, so keep going whilehasMoreistrue.
{
"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
pagebelow 1, or one that isn't a number, becomes1. - A
limitabove the maximum becomes the maximum: 100 for sources, 50 for logs. - A
limitof0, or one that isn't a number, becomes the default: 20 for sources, 10 for logs. - A negative
limitbecomes1. - The page metadata sits at the top level of the response next to
success. There is nohasMore, so compute it frompage * limit < total.
{
"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.
limitmust be an integer from 1 to 100. Values outside that range, or that aren't a number, are rejected with422.- There is no
pageoroffsetparameter. Every request returns the newestlimitjobs, andpagination.pageis always1. - The metadata includes
totalPagesrather thanhasMore.
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.
skipis the number of chats to pass over. A negative value, or one that isn't a number, becomes0.limitabove 100 becomes 100. Alimitof0, or one that isn't a number, becomes the default of 20. A negativelimitbecomes1.pagination.hasMoreistruewhileskip + limitis less thantotal.sortBycan beupdatedAt(default),createdAtorchatId.sortOrdercan beascordesc(default).
{
"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:
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:
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
limitwithin range. A value one over the maximum fails the whole request rather than being reduced.