Guides

Sync your catalog

Mirror the whole catalog into an ERP, data warehouse, or storefront, then stay current with a poll that usually costs a handful of requests. One full cursor walk up front; updated_since and ETags forever after.

Before you start

You need an API key with pim:read (created in Settings → API — see Authentication). Verify it end to end first:

curl -s https://api.peak-pim.com/api/v1/ping \
  -H "Authorization: Bearer pk_live_..."

{"status":"ok","account_id_present":true}

Step 1 — the initial full walk

Each list endpoint returns one cursor page, ordered oldest change first, so a walker can page through the whole catalog and stop when next_cursor is null. Ask for full pages — limit=250 is the maximum (the default is 50; values above the maximum are clamped):

# First page
curl -s "https://api.peak-pim.com/api/v1/products?limit=250" \
  -H "Authorization: Bearer pk_live_..."

{"data": [
  {"id": "0e8f...", "type": "product", "title": "Trail Runner 2",
   "draft_revision": 7, "pending_publish": false,
   "last_publish_status": "published", "store_ids": ["a1b2...", "c3d4..."]},
  ...
], "next_cursor": "eyJ..."}

# Every following page: pass next_cursor back as cursor
curl -s "https://api.peak-pim.com/api/v1/products?limit=250&cursor=eyJ..." \
  -H "Authorization: Bearer pk_live_..."

Repeat for each resource you mirror — /api/v1/variants, /api/v1/collections, /api/v1/media — and read GET /api/v1/stores once to know the account's stores. Cursors are opaque: never build or parse one, and never reuse one across lists (a cursor from a different list is rejected with 400 validation_failed).

List rows are lean. When your mirror needs the values themselves, follow up per entity:

ReadGives you
GET /api/v1/products/{id}Canonical attributes, variant_ids, media_ids, per-store publish state
GET /api/v1/products/{id}/storesEvery store version's attributes in one response (next_cursor always null)

Step 2 — incremental polling with updated_since

Record a timestamp before the walk starts. From then on, each poll passes it as updated_since (RFC3339) so unchanged entities never appear in the page at all:

curl -s "https://api.peak-pim.com/api/v1/products?limit=250&updated_since=2026-07-30T00:00:00Z" \
  -H "Authorization: Bearer pk_live_..."

A quiet day answers an empty data array; on a busy day you page through with cursor as usual, then advance your stored timestamp. A non-RFC3339 value is rejected with 400 validation_failed.

The store-version caveat.

updated_since matches an entity's own canonical record — its shared values and structure. Editing one store's version of an entity does not widen the filter. A poller that must catch store-local edits should re-read the store versions (GET /api/v1/{resource}/{id}/stores) of the entities it tracks.

Step 3 — conditional reads with ETags

Every 200 response carries an ETag. Send it back as If-None-Match and an unchanged response becomes a bodyless 304:

curl -s -D - -o /dev/null \
  "https://api.peak-pim.com/api/v1/products?limit=250&updated_since=2026-07-30T00:00:00Z" \
  -H "Authorization: Bearer pk_live_..." \
  -H 'If-None-Match: "3f2a..."'

HTTP/1.1 304 Not Modified

Cache the ETag per exact URL you poll. A 304 tells you nothing changed and costs no bandwidth — the ideal steady state for a recurring sync.

Step 4 — stay inside the rate budget

The budgets are 120 requests/minute per key and 240/minute per account, both enforced. A well-behaved sync barely touches them:

The whole daily poll, end to end

# 1. One conditional, incremental list per mirrored resource
GET /api/v1/products?limit=250&updated_since={last_run}     # If-None-Match: {etag}
GET /api/v1/variants?limit=250&updated_since={last_run}
GET /api/v1/collections?limit=250&updated_since={last_run}
GET /api/v1/media?limit=250&updated_since={last_run}

# 2. For each changed id: canonical values + store versions
GET /api/v1/products/{id}
GET /api/v1/products/{id}/stores

# 3. Advance last_run, store the new ETags

Four requests on a quiet day, all potentially 304s. Within v1 the contract only grows — new fields may appear at any time, so ignore response fields you don't recognize (see Versioning).