Using the API
Pagination & filtering
Every list on the API is {"data": [...], "next_cursor": ...}. The
four catalog lists paginate with an opaque cursor; every other list is bounded and
returns everything at once; one endpoint — data-health issues — is page-numbered.
Cursor pagination
The catalog lists — /api/v1/products, /variants,
/collections, /media — return one page at a time,
ordered oldest change first. Pass next_cursor back as
cursor for the next page; it is null on the last page,
so a poller can walk the whole catalog and stop:
curl "https://api.peak-pim.com/api/v1/products?limit=250" \
-H "Authorization: Bearer $PEAK_PIM_KEY"
# → {"data": [...], "next_cursor": "eyJv…"}
curl "https://api.peak-pim.com/api/v1/products?limit=250&cursor=eyJv..." \
-H "Authorization: Bearer $PEAK_PIM_KEY"
# → {"data": [...], "next_cursor": null} — last page
- Cursors are opaque. Never build or parse one — the format can change at any time.
- Cursors are per-list. A cursor from a different list is rejected with
400 validation_failedrather than silently skipping rows. limitdefaults to 50 and is clamped at 250 — a value above the maximum is clamped, a non-numeric value falls back to the default. Ask for full pages (limit=250) to spend the least rate-limit budget.
Bounded lists
Every other list is bounded by something small — the account's stores, the
account itself, one definition, or locales × stores — so the whole set is
returned in one response and next_cursor is always
null. Same envelope, no walking:
GET /api/v1/storesand the per-entity store-version lists (/{resource}/{id}/stores).- The operations lists: markets, metafield definitions, metaobject definitions and their entries, translations, sales channels, and drops (account-wide and per-entity).
The one exception: data-health issues
GET /api/v1/data-health/issues is page-numbered rather than
cursor-paged, because a section is a live query over changing state rather than a
stable list to walk. It takes page (default 1) and
page_size (default 25, clamped to 100), and answers with
page and has_next:
curl "https://api.peak-pim.com/api/v1/data-health/issues?section=...&page=2&page_size=100" \
-H "Authorization: Bearer $PEAK_PIM_KEY"
# → {"section": "...", "data": [...], "page": 2, "has_next": false}
Details on sections and issue rows: Data health.
Filtering
The catalog lists take three filters, freely combinable:
| Parameter | Meaning |
|---|---|
updated_since |
Only entities whose own record changed at or after this RFC3339 timestamp — the canonical, cross-store data. Editing one store's version does not widen this filter, so re-read store versions of entities you track when you need store-local changes. A non-RFC3339 value is rejected with validation_failed. |
status |
Only entities in this publish state (never_published / pending / published / failed). This is Peak PIM's publish state, not Shopify's own product status (which is a per-store attribute value). Response values feed straight back into the filter. |
search |
Free-text match, per entity type: products by name, store title, vendor, tags, or a variant SKU; variants by name, SKU, or the parent product's fields; collections by name or store title; media files by filename, tags, or alt text. |
Conditional reads
Every 200 response carries an ETag. Send it back as
If-None-Match to get a 304 Not Modified instead of a
body — cheap polling for anything you fetch repeatedly:
curl -i "https://api.peak-pim.com/api/v1/products/7b2a..." \
-H "Authorization: Bearer $PEAK_PIM_KEY" \
-H 'If-None-Match: "W/abc123"'
# HTTP/2 304 — nothing changed, no body transferred
Full pages (limit=250), updated_since for incremental polls, and ETags for repeat reads together make a recurring sync nearly free — see Rate limits and the sync guide.