Catalog
Products
A product's values exist twice: the canonical values every store inherits, and one store version per store that can override them. These endpoints read and edit both, create products and publish drafts, and expose the product's relationships — its variants, its media files, and where it stands with each store.
List products
/api/v1/productspim:read
One cursor page of the account's products, ordered oldest change first so a
poller can walk the whole catalog and stop when next_cursor is
null.
Query parameters
| Parameter | Type | Description |
|---|---|---|
limit | integer | Rows per page. Default 50, maximum 250; a value above the maximum is clamped and a non-numeric value falls back to the default. |
cursor | string | The next_cursor of the previous page. Opaque — do not build or parse one. A cursor from a different list is rejected with validation_failed. |
updated_since | RFC3339 date-time | Only products whose own canonical record changed at or after this time. Store-version edits do not widen it. A non-RFC3339 value is rejected with validation_failed. |
status | enum | Publish-state filter: never_published, pending, published, failed. This is where the product stands with Peak PIM's stores, not Shopify's own product status. |
search | string | Free-text match on the product's name, store title, vendor, tags, or a variant SKU. |
Response
| Field | Type | Description |
|---|---|---|
data[].id | uuid | The product's id. |
data[].type | string | Always product on this list. |
data[].title | string | The product's name. |
data[].draft_revision | integer | The entity's revision — the optimistic-concurrency handle a later write sends back to prove it read the current state. |
data[].pending_publish | boolean | Draft changes not yet pushed to a store. |
data[].last_publish_status | enum | never_published, pending, published, or failed — the same values the status filter takes, so a response value can be fed straight back into the filter. |
data[].store_ids | uuid[] | The stores the product lives on. |
next_cursor | string or null | Pass it back as cursor for the next page. Null on the last page. |
# Everything that changed since July 1, biggest pages
curl -s "https://api.peak-pim.com/api/v1/products?limit=250&updated_since=2026-07-01T00:00:00Z" \
-H "Authorization: Bearer pk_live_..."
{
"data": [
{
"id": "5e0deff8-4b6f-4a24-9d6d-2f8c2f2a9a11",
"type": "product",
"title": "Trail Runner 2",
"draft_revision": 14,
"pending_publish": true,
"last_publish_status": "published",
"store_ids": ["9b2f6c1a-7d3e-4f80-b1c5-6a4e8d2b9c30"]
}
],
"next_cursor": "eyJvZmZzZXQiOjI1MH0"
}
Editing one store's version of a product does not widen the filter — a poller that must catch store-local edits should re-read the store versions of the products it tracks. See the sync guide for the full pattern.
Get a product
/api/v1/products/{id}pim:read
One product's canonical values, its variants and media files, and where it
stands with each store it lives on. An unknown id — including a record that
belongs to another account or an id of a different kind of entity — answers
404 not_found.
| Field | Type | Description |
|---|---|---|
id, type, title | — | As on the list row; type is product. |
attributes | array | The canonical values every store inherits, as {key, value, type} triples (all strings). |
draft_revision, pending_publish, last_publish_status | — | As on the list row. |
variant_ids | uuid[] | The product's variants. Fetch each via GET /api/v1/variants/{id}. |
media_ids | uuid[] | The media files this product uses, in gallery order, across every store. |
stores[] | array | Publish state per store: store_id, store_domain, pending_publish, last_publish_status. Publish state only — the store's values live on the store subresources below. |
{
"id": "5e0deff8-4b6f-4a24-9d6d-2f8c2f2a9a11",
"type": "product",
"title": "Trail Runner 2",
"attributes": [
{ "key": "Vendor", "value": "Acme", "type": "single_line_text_field" },
{ "key": "Tags", "value": "trail,running", "type": "single_line_text_field" }
],
"draft_revision": 14,
"pending_publish": true,
"last_publish_status": "published",
"variant_ids": ["c7a1d2e3-5b6f-4890-a1b2-c3d4e5f60718"],
"media_ids": ["1f2e3d4c-5b6a-4798-8172-635a4b3c2d1e"],
"stores": [
{
"store_id": "9b2f6c1a-7d3e-4f80-b1c5-6a4e8d2b9c30",
"store_domain": "acme.myshopify.com",
"pending_publish": true,
"last_publish_status": "published"
}
]
}
List a product's store versions
/api/v1/products/{id}/storespim:read
Every store version of the product — the values each store actually publishes,
which may differ from every other store's. Bounded by the account's stores, so
the whole list is returned and next_cursor is always
null.
{
"data": [
{
"store_id": "9b2f6c1a-7d3e-4f80-b1c5-6a4e8d2b9c30",
"store_domain": "acme.myshopify.com",
"revision": "sv1_<opaque token from the API>",
"attributes": [
{ "key": "Title", "value": "Trail Runner 2 — US Edition", "type": "single_line_text_field" }
],
"pending_publish": false,
"last_publish_status": "published"
}
],
"next_cursor": null
}
Get one store's version
/api/v1/products/{id}/stores/{store_id}pim:read
One store's version of the product. store_id comes from
GET /api/v1/stores. A product with no
version on the requested store answers 404 not_found — the same
answer as a nonexistent product.
curl -s "https://api.peak-pim.com/api/v1/products/5e0deff8-4b6f-4a24-9d6d-2f8c2f2a9a11/stores/9b2f6c1a-7d3e-4f80-b1c5-6a4e8d2b9c30" \
-H "Authorization: Bearer pk_live_..."
Create a product
/api/v1/productspim:writeCreate a product draft and its default variant on the explicitly selected stores. store_ids must contain at least one linked store, with no duplicates. Canonical attributes seed each store version; optional store_attributes overrides values by selected store ID. The whole creation succeeds or rolls back together. The response is 201 with the product in the same shape as the detail read.
curl -sS -X POST "https://api.peak-pim.com/api/v1/products" \
-H "Authorization: Bearer $PEAK_PIM_KEY" \
-H "Content-Type: application/json" \
--data '{
"attributes": [
{
"key": "Internal Name",
"value": "Trail Runner 2",
"type": "single_line_text_field"
},
{
"key": "Title",
"value": "Trail Runner 2",
"type": "single_line_text_field"
}
],
"store_ids": [
"9b2f6c1a-7d3e-4f80-b1c5-6a4e8d2b9c30"
],
"store_attributes": {
"9b2f6c1a-7d3e-4f80-b1c5-6a4e8d2b9c30": [
{
"key": "Title",
"value": "Trail Runner 2 — EU",
"type": "single_line_text_field"
}
]
}
}'
Creation saves drafts only. No change reaches Shopify until you publish. There are no idempotency keys: if a create response is lost, inspect the catalog before retrying to avoid creating a duplicate.
Starting a product with real variants
Declare initial option dimensions in the product’s store_attributes when creating it, then create the variants before publishing. For example, a selected store can receive these attributes alongside its Title:
[
{"key": "Option 1 Name", "value": "Size", "type": "single_line_text_field"},
{"key": "Option 1 Values", "value": "[\"S\",\"M\"]", "type": "list.single_line_text_field"}
]
Each variant then sets Option: Size on that store. Initial option positions must be consecutive (1–3), names unique and non-empty, and each dimension must declare values. Later option-dimension editing is outside these public endpoints.
Update canonical values
/api/v1/products/{id}pim:writeRead the entity first, then send its integer draft_revision as revision with a changes object. Zero is valid for a newly created or imported record. Attribute keys are case-sensitive: use Title, Vendor or SKU, exactly as returned. Only the named canonical fields change; existing store versions keep their own values. To change what Shopify receives, update the relevant store version below.
{
"revision": 14,
"changes": {
"Title": "Trail Runner 3"
}
}
Update one store’s values and metafields
/api/v1/products/{id}/stores/{store_id}pim:writeRead the store version and copy its opaque string revision into the PATCH body. This token is specific to the entity and store, and differs from the canonical integer revision and the HTTP ETag. The response contains the updated store version and revision. A stale token saves nothing and returns 409 conflict with error.current_revision; read again before reapplying the intended edit.
{
"revision": "<revision from this store read>",
"changes": {
"mf:custom.material": "Cotton"
}
}
Metafields are ordinary attributes with keys mf:<namespace>.<key>. Read the account’s metafield definitions for the owner type and value type before writing. JSON, list and rich-text values are JSON encoded inside the value string. An empty string clears a value. Invalid typed values refuse the complete request; computed fields remain read-only. These endpoints change values, not metafield definitions.
Publish and follow the result
/api/v1/products/{id}/publishpim:publishName the target stores explicitly in a non-empty store_ids array. Every selected store must carry this product. Publishing a product also sends its eligible variants. The response is 202 with an operation; acceptance does not mean the changes are live.
{
"store_ids": [
"9b2f6c1a-7d3e-4f80-b1c5-6a4e8d2b9c30"
]
}
/api/v1/publish-operations/{operation_id}pim:readPoll with the returned operation ID while status is queued or running. Terminal statuses are completed, failed and canceled; inspect each store’s outcome. An operation can finish while some stores failed. Operation records remain readable for 24 hours; missing, expired, foreign-account, and other kinds of operation all return 404 not_found.
{
"operation": {
"id": "d5528b76-6bd1-41ea-9e37-b67ce290369a",
"status": "completed",
"store_ids": [
"9b2f6c1a-7d3e-4f80-b1c5-6a4e8d2b9c30"
],
"created_at": "2026-10-27T10:00:00Z",
"finished_at": "2026-10-27T10:00:05Z",
"counts": {
"total": 1,
"completed": 1,
"succeeded": 1,
"failed": 0
},
"entities": [
{
"entity_id": "5e0deff8-4b6f-4a24-9d6d-2f8c2f2a9a11",
"entity_type": "product",
"status": "ok",
"stores": [
{
"store_id": "9b2f6c1a-7d3e-4f80-b1c5-6a4e8d2b9c30",
"status": "ok"
}
]
}
]
}
}
Variant parent confirmation
A variant whose parent product has not been created on Shopify may require 409 parent_publish_confirmation_required. Review the returned products, stores and variant counts: this action also publishes the parent and eligible sibling variants. Only after approving that scope, resend the same stores with parent_publish_confirmation: {"fingerprint": "<returned fingerprint>"}. An obsolete fingerprint produces a fresh challenge; it never silently widens the publication.
A 423 pim_locked means import or refresh reconciliation must finish first. A 409 publish_lock_conflict means a running operation owns records needed by this write. See Errors for recovery.
Subresources
Products carry three more reads, documented on their own pages:
| Endpoint | Returns | Docs |
|---|---|---|
GET /api/v1/products/{id}/translations | Every locale the product is translated into, with the translated values per store, plus the catalog of fields that can carry a translation. | Translations |
GET /api/v1/products/{id}/sales-channels?store_id=… | The Shopify sales channels the product is published to on one store. Reaches Shopify live, so it can answer 502. | Sales channels |
GET /api/v1/products/{id}/drops | The drops that will change this product (or its variants), each narrowed to the changes that touch it. | Drops |