Pagination
Every list endpoint returns the same base envelope:
{
"data": [
/* ... */
],
"pagination": { "nextCursor": "opaque-string-or-null", "hasMore": true, "pageSize": 20 }
}
Generation-plane lists (GET /presets, GET /runs, GET /runs/{id}/items) add an "object": "list" field on top of this same shape, following the object type-tag convention described in
the changelog. Catalog, Campaigns & Creatives, and Deliveries list envelopes
don't carry that field — data and pagination are all you get, either way.
data— the page of results.pagination.nextCursor— an opaque string, ornullon the last page. Pass it back verbatim as?cursor=to get the next page.pagination.hasMore— a convenience mirror ofnextCursor !== null. It's provided so you don't have to compute it yourself, but it's derived fromnextCursor, not an independent signal.pagination.pageSize— the page size actually applied to this response.
Request a page size with ?limit=, and continue with ?cursor=:
curl -s "https://api.mocart.io/api/public/v1/products?brandId=$BRAND_ID&limit=50" \
-H "Authorization: Bearer $API_KEY"
curl -s "https://api.mocart.io/api/public/v1/products?brandId=$BRAND_ID&limit=50&cursor=$NEXT_CURSOR" \
-H "Authorization: Bearer $API_KEY"
The one rule: iterate on nextCursor, never on data
Keep paginating while pagination.nextCursor is non-null. Never stop just because data came
back short or empty. A page can be legitimately sparse — or fully empty — without being the last
one, because results can be filtered out after the underlying query runs while a further page
still exists. nextCursor === null is the only reliable end-of-data signal.
A loop that stops on data.length === 0 will silently truncate results on some accounts and not
others, which is a difficult bug to notice in testing and a worse one to notice in production.
Node: an async iterator over any list endpoint
async function* paginate(path, params = {}) {
let cursor;
do {
const query = new URLSearchParams({ ...params, ...(cursor ? { cursor } : {}) });
const page = await mocart(`${path}?${query}`); // see the Quickstart for `mocart()`
yield* page.data;
cursor = page.pagination.nextCursor;
} while (cursor !== null);
}
for await (const product of paginate('/products', { brandId: BRAND_ID, limit: 50 })) {
console.log(product.productId, product.title);
}
This loop is correct regardless of how sparse any individual page turns out to be — it only ever
stops when nextCursor is null.
Every list endpoint uses this same data + pagination shape, including GET /presets
(listPresets) — its catalog is small and global, so in practice you'll see hasMore: false on
the first page, but the iteration rule is identical to every other list response.