Catalog

Variants

A variant belongs to exactly one product — its product_id — and, like every entity, its values exist twice: the canonical values every store inherits and one store version per store that can override them. Variants are a top-level list, so an integration can read and update their values directly. Create beneath a product, edit drafts, then publish to the selected stores.

List variants

GET/api/v1/variantspim:read

One cursor page of the account's variants, 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 variants 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.
searchstringFree-text match on the variant's name, SKU, or the parent product's name, title, vendor, or tags.

Response

The same list-item shape as every entity list: id, type (here variant), title, draft_revision, pending_publish, last_publish_status, and store_ids. last_publish_status takes the same values as the status filter, so a response value can be fed straight back in.

# Find a variant by SKU
curl -s "https://api.peak-pim.com/api/v1/variants?search=TR2-BLK-42&limit=50" \
  -H "Authorization: Bearer pk_live_..."
{
  "data": [
    {
      "id": "c7a1d2e3-5b6f-4890-a1b2-c3d4e5f60718",
      "type": "variant",
      "title": "Trail Runner 2 — Black / 42",
      "draft_revision": 6,
      "pending_publish": false,
      "last_publish_status": "published",
      "store_ids": ["9b2f6c1a-7d3e-4f80-b1c5-6a4e8d2b9c30"]
    }
  ],
  "next_cursor": null
}

Get a variant

GET/api/v1/variants/{id}pim:read

One variant's canonical values, the product it belongs to (product_id), its media files, and its per-store publish state. An unknown id — including another account's — answers 404 not_found.

FieldTypeDescription
id, type, title—As on the list row; type is variant.
attributesarrayThe canonical values every store inherits, as {key, value, type} triples (all strings).
draft_revisionintegerThe optimistic-concurrency handle a later write sends back to prove it read the current state.
pending_publish, last_publish_status—As on the list row.
product_iduuidThe product this variant belongs to. Fetch it via GET /api/v1/products/{id}.
media_idsuuid[]The media files this variant uses, in gallery order, across every store.
stores[]arrayPublish state per store: store_id, store_domain, pending_publish, last_publish_status. Publish state only — values live on the store subresources below.
{
  "id": "c7a1d2e3-5b6f-4890-a1b2-c3d4e5f60718",
  "type": "variant",
  "title": "Trail Runner 2 — Black / 42",
  "attributes": [
    { "key": "SKU", "value": "TR2-BLK-42", "type": "single_line_text_field" },
    { "key": "price", "value": "129.00", "type": "single_line_text_field" }
  ],
  "draft_revision": 6,
  "pending_publish": false,
  "last_publish_status": "published",
  "product_id": "5e0deff8-4b6f-4a24-9d6d-2f8c2f2a9a11",
  "media_ids": ["1f2e3d4c-5b6a-4798-8172-635a4b3c2d1e"],
  "stores": [
    {
      "store_id": "9b2f6c1a-7d3e-4f80-b1c5-6a4e8d2b9c30",
      "store_domain": "acme.myshopify.com",
      "pending_publish": false,
      "last_publish_status": "published"
    }
  ]
}
Variants carry no variant_ids.

variant_ids exists on products only, and product_id on variants only — the relationship is stored once and readable from both ends.

List a variant's store versions

GET/api/v1/variants/{id}/storespim:read

Every store version of the variant. Bounded by the account's stores, so the whole list is returned and next_cursor is always null. Each item carries store_id, store_domain, attributes, pending_publish, and last_publish_status.

{
  "data": [
    {
      "store_id": "9b2f6c1a-7d3e-4f80-b1c5-6a4e8d2b9c30",
      "store_domain": "acme.myshopify.com",
      "revision": "sv1_<opaque token from the API>",
      "attributes": [
        { "key": "price", "value": "119.00", "type": "single_line_text_field" }
      ],
      "pending_publish": false,
      "last_publish_status": "published"
    }
  ],
  "next_cursor": null
}

Get one store's version

GET/api/v1/variants/{id}/stores/{store_id}pim:read

One store's version of the variant: the values that store publishes — a per-store price, for instance — which may differ from every other store's. store_id comes from GET /api/v1/stores. A variant with no version on the requested store answers 404 not_found.

curl -s "https://api.peak-pim.com/api/v1/variants/c7a1d2e3-5b6f-4890-a1b2-c3d4e5f60718/stores/9b2f6c1a-7d3e-4f80-b1c5-6a4e8d2b9c30" \
  -H "Authorization: Bearer pk_live_..."

Create a variant

POST/api/v1/products/{product_id}/variantspim:write

Create a draft variant beneath the product named in the path. Every selected store must already carry that product. Set selected options with Option: <name> keys, using dimensions already declared on the product; the example assumes a Size dimension. Duplicate option combinations are refused. A selected value is declared on that store’s product in the same transaction. The response is 201 with the variant in the same shape as the detail read.

curl -sS -X POST "https://api.peak-pim.com/api/v1/products/5e0deff8-4b6f-4a24-9d6d-2f8c2f2a9a11/variants" \
  -H "Authorization: Bearer $PEAK_PIM_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "attributes": [
    {
      "key": "Internal Name",
      "value": "Trail Runner 2 / M",
      "type": "single_line_text_field"
    }
  ],
  "store_ids": [
    "9b2f6c1a-7d3e-4f80-b1c5-6a4e8d2b9c30"
  ],
  "store_attributes": {
    "9b2f6c1a-7d3e-4f80-b1c5-6a4e8d2b9c30": [
      {
        "key": "SKU",
        "value": "TRAIL-M",
        "type": "single_line_text_field"
      },
      {
        "key": "Option: Size",
        "value": "M",
        "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.

Update canonical values

PATCH/api/v1/variants/{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/variants/{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/variants/{id}/publishpim:publish

Name the target stores explicitly in a non-empty store_ids array. Every selected store must carry this variant. 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": "variant",
        "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

Variants carry two more reads, documented on their own pages:

EndpointReturnsDocs
GET /api/v1/variants/{id}/sales-channels?store_id=…The Shopify sales channels the variant is published to on one store. Reaches Shopify live, so it can answer 502.Sales channels
GET /api/v1/variants/{id}/dropsThe drops that will change this variant, each narrowed to the changes that touch it.Drops