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:
| Read | Gives you |
|---|---|
GET /api/v1/products/{id} | Canonical attributes, variant_ids, media_ids, per-store publish state |
GET /api/v1/products/{id}/stores | Every 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.
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:
- Full pages.
limit=250turns a 5,000-product walk into 20 requests. - Incremental first. Poll lists with
updated_since; only fetch detail for the ids that actually appeared. - Watch the headers. Every response carries
X-RateLimit-Limit,X-RateLimit-Remaining, andX-RateLimit-Reset. - Retry correctly. On
429waitRetry-Afterseconds and retry; on5xxretry with exponential backoff and jitter; other4xxare not retryable unchanged.
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).
Related
- Pagination & filtering — the full parameter reference.
- Rate limits — budgets, headers, and 429 handling.
- Audit drift across stores — put the mirrored store versions to work.