Skip to content

Catalog delta sync

If you mirror the scent catalog (scents, brands, variants) into your own system, don’t re-fetch the entire catalog on every sync — use the changedSince query parameter, supported identically on all three list endpoints:

  • GET /api/v1/scent/catalog/scents?changedSince=...
  • GET /api/v1/scent/catalog/brands?changedSince=...
  • GET /api/v1/scent/catalog/variants?changedSince=...

changedSince takes an ISO-8601 UTC timestamp and returns only rows changed strictly after that instant. When set, results are ordered by change time ascending (oldest-changed first) instead of the endpoint’s normal default ordering — this matters because it means you can safely paginate through a large batch of changes and stop wherever you like, resuming later from the timestamp of the last row you actually processed.

Terminal window
GET /api/v1/scent/catalog/variants?changedSince=2026-08-01T00:00:00Z&pageSize=100
  1. On first sync, omit changedSince entirely and page through the full catalog, recording the response timestamp (or the lastModifiedDate/createdDate of the last row you processed) as your cursor.
  2. On every subsequent sync, call the same endpoint with changedSince set to your last cursor.
  3. Process every returned row, then advance your cursor to the latest change time you actually saw in that batch — not simply “now,” since a row could theoretically be committed with a change time slightly behind your sync’s start time under concurrent writes.
  4. If a batch returns a full page (pageSize items), keep paging with the same changedSince value until you get a partial or empty page — the endpoint does not auto-advance your cursor for you between pages.
async function syncSince(cursor) {
let page = 1;
let latestSeen = cursor;
while (true) {
const res = await client.get('/api/v1/scent/catalog/variants', {
params: { changedSince: cursor, page, pageSize: 100 },
});
const { items, pagination } = res.data.payload;
for (const item of items) {
await upsertLocalVariant(item);
const changedAt = item.lastModifiedDate ?? item.createdDate;
if (changedAt > latestSeen) latestSeen = changedAt;
}
if (page >= pagination.totalPages) break;
page++;
}
return latestSeen;
}

A row that has never been updated since creation has a null lastModifiedDate — the endpoint falls back to createdDate internally when evaluating changedSince, so a never-updated row is still correctly included/excluded relative to your cursor. You don’t need to handle this case specially on your side.