Pagination & fields
Every endpoint that returns a collection uses one shape, and every one of them supports trimming the payload down to the fields you actually render.
The list shape
{
"status": true,
"status_code": 200,
"message": "",
"data": {
"items": [ /* … */ ],
"next_cursor": "eyJ2IjoxLCJzIjoibmV3ZXN0Iiwi…",
"has_more": true
}
}Some endpoints add a sibling next to items — product reviews include summary, and category products include category — but items, next_cursor and has_more are always present.
Endpoints that return a small, complete set (categories, payment methods, curated lists like top products) use the same shape with next_cursor: null and has_more: false, so one code path handles every list.
Walking pages
Send next_cursor back as cursor, and stop when has_more is false:
async function allProducts(baseUrl, key) {
const out = [];
let cursor = null;
do {
const url = new URL(`${baseUrl}/api/v1/business/website/products/`);
url.searchParams.set("limit", "50");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers: { "X-Periscale-Key": key } });
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const { data } = await res.json();
out.push(...data.items);
cursor = data.has_more ? data.next_cursor : null;
} while (cursor);
return out;
}Why cursors instead of page numbers
Pagination is keyset-based, not offset-based. Pages stay correct while the catalog changes underneath you: with ?page=2, a product added during the crawl shifts everything down and you silently get a duplicate or skip an item. A cursor points at a position in the sort order, so that cannot happen.
Two rules:
- A cursor belongs to its sort. It encodes the position within a specific ordering, so changing
sortmid-walk is rejected — start again from page one. - Cursors are opaque. Treat them as strings. Do not parse, build, or persist them long-term; the encoding may change.
An invalid or tampered cursor returns 400 with error_code: validation_failed.
Limits
| Endpoint group | Default | Max |
|---|---|---|
| Products, category products, flash deals, bundles | 24 | 100 |
| Reviews | 20 | 50 |
| Blog posts | 20 | 100 |
Values above the maximum are clamped rather than rejected.
Selecting fields
Data endpoints accept fields — a comma-separated list of top-level fields:
GET /api/v1/business/website/products/?fields=id,name,price,images{ "data": { "items": [
{ "id": 148, "name": "Sweet Orange Marmalade", "price": "1700.00", "images": [] }
], "next_cursor": null, "has_more": false } }idis always included, whether or not you ask for it.- Unknown field names are ignored, so a typo yields fewer fields rather than an error.
- Omit
fieldsto get the full object.
This is not only a smaller response. Asking for fewer fields also skips the work that would have populated the omitted ones — a product list without variants or reviews avoids those lookups entirely, which is a real speed-up on a large catalog. Listing pages that only need a card's worth of data benefit most.
Combine with pagination
?fields=id,slug,name,price&limit=100 is the fastest way to walk a full catalog — for example to build a sitemap or a search index.