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

GET/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

ParameterTypeDescription
limitintegerRows per page. Default 50, maximum 250; a value above the maximum is clamped and a non-numeric value falls back to the default.
cursorstringThe 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_sinceRFC3339 date-timeOnly 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.
statusenumPublish-state filter: never_published, pending, published, failed. This is where the product stands with Peak PIM's stores, not Shopify's own product status.
searchstringFree-text match on the product's name, store title, vendor, tags, or a variant SKU.

Response

FieldTypeDescription
data[].iduuidThe product's id.
data[].typestringAlways product on this list.
data[].titlestringThe product's name.
data[].draft_revisionintegerThe entity's revision — the optimistic-concurrency handle a later write sends back to prove it read the current state.
data[].pending_publishbooleanDraft changes not yet pushed to a store.
data[].last_publish_statusenumnever_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_idsuuid[]The stores the product lives on.
next_cursorstring or nullPass 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"
}
updated_since watches the canonical record only.

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

GET/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.

FieldTypeDescription
id, type, title—As on the list row; type is product.
attributesarrayThe 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_idsuuid[]The product's variants. Fetch each via GET /api/v1/variants/{id}.
media_idsuuid[]The media files this product uses, in gallery order, across every store.
stores[]arrayPublish 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

GET/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

GET/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

POST/api/v1/productspim:write

Create 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

PATCH/api/v1/products/{id}pim:write

Read 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

PATCH/api/v1/products/{id}/stores/{store_id}pim:write

Read 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

POST/api/v1/products/{id}/publishpim:publish

Name 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"
  ]
}
GET/api/v1/publish-operations/{operation_id}pim:read

Poll 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:

EndpointReturnsDocs
GET /api/v1/products/{id}/translationsEvery 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}/dropsThe drops that will change this product (or its variants), each narrowed to the changes that touch it.Drops