Pagination & incremental sync
Every list endpoint pages the same way, and product lists can be limited to what changed since your last sync — the cheapest way to keep an ERP, storefront or search index up to date.
Pages
Send page (from 1) and limit (1–250, default 50). Paginated responses carry a pagination block; stop when hasMore is false.
pagination
json
{
"page": 2,
"limit": 250,
"total": 1284,
"totalPages": 6,
"hasMore": true
}Small, bounded collections — categories, attributes, attribute groups, collections, channels — are returned whole as { data: [...] } without a pagination block.
Incremental sync with updated_since
- Remember when a sync started and pass it as
updated_sincenext time, withsort=updatedAt:asc. Using the start time (not the end time) means edits made while you were syncing are picked up again rather than missed. - Every change — in the app, through the API, an import, a bulk edit or AI enrichment — updates the product's
updatedAtand bumps itsversion. Changing a product's collections updates it too. - Pages are computed per request: if products change while you page, some may move between pages. Syncing oldest-first and re-reading from your saved start time makes this harmless — upserts by SKU are idempotent.
- Deletions do not appear in lists. Subscribe to the
product.deletedwebhook, or periodically compare the SKU list (GET /products?limit=250) with yours.
Node.js
javascript
const BASE = "https://library.retailcommerceai.com/api/v1";
const headers = { Authorization: `Bearer ${process.env.CL_API_KEY}` };
/** Every product changed since `since` (ISO string), oldest change first. */
async function* changedSince(since) {
for (let page = 1; ; page++) {
const url = `${BASE}/products?updated_since=${encodeURIComponent(since)}&sort=updatedAt:asc&limit=250&page=${page}`;
const res = await fetch(url, { headers });
if (res.status === 429) {
await new Promise((r) => setTimeout(r, Number(res.headers.get("retry-after") ?? 1) * 1000));
page--; // retry the same page
continue;
}
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const { data, pagination } = await res.json();
yield* data;
if (!pagination.hasMore) return;
}
}
const startedAt = new Date().toISOString();
let cursor = await loadCursor(); // e.g. from your database; first run: "1970-01-01T00:00:00Z"
for await (const product of changedSince(cursor)) {
await upsertIntoErp(product);
}
await saveCursor(startedAt);Filters
Product lists (and exports, through the same filter object) accept:
| Parameter | Meaning |
|---|---|
q | Text search in name, SKU, GTIN, MPN and brand |
sku / ids | Comma-separated SKUs or product ids (up to 500) |
status | draft, in_review, approved, archived (comma-separated) |
type | simple, parent, variant |
category | Category ids or codes; subcategories are included unless include_subcategories=false |
collection | Collection ids or codes |
tag / brand | Tag(s) and exact brand |
updated_since | ISO-8601 timestamp — products changed at or after it |
completeness_min / completeness_max | 0–100 |
has_images | true or false |
top_level | true: simple and parent products only |
sort | updatedAt:desc (default), updatedAt:asc, createdAt:desc, name:asc|desc, sku:asc|desc, price:asc|desc, completeness:asc|desc — or -updated_at style |
include | variants — nest variants under their parent (lists then show top-level products) |
locale | Show content in another workspace language, falling back per field |
Polling one product cheaply
Product responses carry an ETag. Send it back as If-None-Match and the API answers 304 Not Modified with no body while the product is unchanged.
Prefer webhooks for real-time
Polling every few minutes is fine for nightly or hourly syncs. For near real-time updates, register a webhook for
product.updated and fetch the product when it fires.