openapi: 3.1.0
info:
  title: Peak PIM Public API
  version: "1.0.0"
  summary: Read your Peak PIM catalog from any system.
  description: |
    The versioned, supported surface third-party systems program against. It is
    separate from the application's internal API, which changes freely.

    Authentication. Every request carries an API key as
    `Authorization: Bearer pk_live_...`. Keys are created by a store user in
    Settings -> API, are scoped (`pim:read`, `pim:write`, `pim:publish`,
    `pim:webhooks`), belong to exactly one account, and are revocable at any
    time — a revoked key stops working on its next request. A key can only
    reach `/api/v1` paths, and only an API key can reach them: a browser or
    Shopify session is refused here.

    Vocabulary. The API speaks in products, variants, collections, media files,
    and stores. A product's values exist twice: the canonical values every
    store inherits, and one store version per store that can override them.

    Compatibility. Within v1 changes are additive only: new fields and new
    endpoints may appear at any time, so clients must ignore response fields
    they do not know. A field removal, retype, or semantic change would ship as
    `/api/v2`. Deprecations are announced with `Deprecation` and `Sunset`
    headers and in docs/api/CHANGELOG.md.

    Errors. Every failure is
    `{"error": {"code": ..., "message": ..., "request_id": ...}}`. Branch on
    `code`, never on `message`. Two codes are not reachable on the read
    endpoints in this version but belong to the published set:
    `pim_locked` (423 — writes are held while an import or store refresh is
    being reconciled) and `conflict` (409 — a stale `draft_revision` on a
    write).

    Rate limits. 120 requests/minute per key and 240/minute per account. Every
    response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and
    `X-RateLimit-Reset`; over budget is a 429 with `Retry-After`.

    Conditional reads. Every 200 response carries an `ETag`; send it back as
    `If-None-Match` to get a 304 instead of a body.
servers:
  - url: https://app.peak-pim.com
    description: Production
security:
  - apiKey: []
tags:
  - name: Products
  - name: Variants
  - name: Collections
  - name: Media
  - name: Stores
  - name: Markets
  - name: Definitions
  - name: Translations
  - name: Sales channels
  - name: Drops
  - name: Data health
  - name: Service
paths:
  /api/v1/ping:
    get:
      tags: [Service]
      operationId: ping
      summary: Verify a key
      description: >
        Answers 200 once a key has passed authentication, the route allowlist,
        its scope, and the rate limiter. Use it to check a new key end to end
        before wiring real calls.
      x-required-scope: pim:read
      responses:
        "200":
          description: The key is valid.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  account_id_present:
                    type: boolean
                required: [status, account_id_present]
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/products:
    get:
      tags: [Products]
      operationId: listProducts
      summary: List products
      description: >
        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.
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/UpdatedSince"
        - $ref: "#/components/parameters/Status"
        - $ref: "#/components/parameters/Search"
      responses:
        "200":
          $ref: "#/components/responses/EntityListPage"
        "400":
          $ref: "#/components/responses/ValidationFailed"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/products/{id}:
    get:
      tags: [Products]
      operationId: getProduct
      summary: Get a product
      description: >
        One product's canonical values, its variants and media files, and where
        it stands with each store it lives on.
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
      responses:
        "200":
          $ref: "#/components/responses/Entity"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/products/{id}/stores:
    get:
      tags: [Products]
      operationId: listProductStores
      summary: List a product's store versions
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
      responses:
        "200":
          $ref: "#/components/responses/StoreVersionList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/products/{id}/stores/{store_id}:
    get:
      tags: [Products]
      operationId: getProductStore
      summary: Get one store's version of a product
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
        - $ref: "#/components/parameters/StoreId"
      responses:
        "200":
          $ref: "#/components/responses/StoreVersion"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/variants:
    get:
      tags: [Variants]
      operationId: listVariants
      summary: List variants
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/UpdatedSince"
        - $ref: "#/components/parameters/Status"
        - $ref: "#/components/parameters/Search"
      responses:
        "200":
          $ref: "#/components/responses/EntityListPage"
        "400":
          $ref: "#/components/responses/ValidationFailed"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/variants/{id}:
    get:
      tags: [Variants]
      operationId: getVariant
      summary: Get a variant
      description: >
        One variant's canonical values, the product it belongs to
        (`product_id`), its media files, and its per-store publish state.
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
      responses:
        "200":
          $ref: "#/components/responses/Entity"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/variants/{id}/stores:
    get:
      tags: [Variants]
      operationId: listVariantStores
      summary: List a variant's store versions
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
      responses:
        "200":
          $ref: "#/components/responses/StoreVersionList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/variants/{id}/stores/{store_id}:
    get:
      tags: [Variants]
      operationId: getVariantStore
      summary: Get one store's version of a variant
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
        - $ref: "#/components/parameters/StoreId"
      responses:
        "200":
          $ref: "#/components/responses/StoreVersion"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/collections:
    get:
      tags: [Collections]
      operationId: listCollections
      summary: List collections
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/UpdatedSince"
        - $ref: "#/components/parameters/Status"
        - $ref: "#/components/parameters/Search"
      responses:
        "200":
          $ref: "#/components/responses/EntityListPage"
        "400":
          $ref: "#/components/responses/ValidationFailed"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/collections/{id}:
    get:
      tags: [Collections]
      operationId: getCollection
      summary: Get a collection
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
      responses:
        "200":
          $ref: "#/components/responses/Entity"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/collections/{id}/stores:
    get:
      tags: [Collections]
      operationId: listCollectionStores
      summary: List a collection's store versions
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
      responses:
        "200":
          $ref: "#/components/responses/StoreVersionList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/collections/{id}/stores/{store_id}:
    get:
      tags: [Collections]
      operationId: getCollectionStore
      summary: Get one store's version of a collection
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
        - $ref: "#/components/parameters/StoreId"
      responses:
        "200":
          $ref: "#/components/responses/StoreVersion"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/media:
    get:
      tags: [Media]
      operationId: listMedia
      summary: List media files
      description: >
        Media files are named by their filename, so `search` matches the
        filename and tags, and `title` carries the filename.
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/UpdatedSince"
        - $ref: "#/components/parameters/Status"
        - $ref: "#/components/parameters/Search"
      responses:
        "200":
          $ref: "#/components/responses/EntityListPage"
        "400":
          $ref: "#/components/responses/ValidationFailed"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/media/{id}:
    get:
      tags: [Media]
      operationId: getMedia
      summary: Get a media file
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
      responses:
        "200":
          $ref: "#/components/responses/Entity"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/media/{id}/stores:
    get:
      tags: [Media]
      operationId: listMediaStores
      summary: List a media file's store versions
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
      responses:
        "200":
          $ref: "#/components/responses/StoreVersionList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/media/{id}/stores/{store_id}:
    get:
      tags: [Media]
      operationId: getMediaStore
      summary: Get one store's version of a media file
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
        - $ref: "#/components/parameters/StoreId"
      responses:
        "200":
          $ref: "#/components/responses/StoreVersion"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/stores:
    get:
      tags: [Stores]
      operationId: listStores
      summary: List linked stores
      description: >
        The Shopify stores linked to the account. Bounded by the account's
        stores, so the whole list is returned and next_cursor is always null.
      x-required-scope: pim:read
      responses:
        "200":
          description: The account's stores.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Store"
                  next_cursor:
                    type: "null"
                required: [data, next_cursor]
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/products/{id}/translations:
    get:
      tags: [Translations]
      operationId: listProductTranslations
      summary: List a product's translations
      description: >
        Every locale this product is translated into, with the translated values
        per store, plus the catalog of fields that can carry a translation.
        Bounded by locales x stores, so the whole list is returned and
        next_cursor is always null.
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
      responses:
        "200":
          $ref: "#/components/responses/EntityTranslations"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/products/{id}/sales-channels:
    get:
      tags: [Sales channels]
      operationId: listProductSalesChannels
      summary: List a product's sales channels on one store
      description: >
        The Shopify sales channels this product is published to on the named
        store. A product that is not on the store, or is on it but was never
        published, is a member of zero sales channels — an empty list, not an
        error. This read reaches Shopify live, so it can answer 502.
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
        - $ref: "#/components/parameters/RequiredStoreIdQuery"
      responses:
        "200":
          $ref: "#/components/responses/SalesChannelList"
        "400":
          $ref: "#/components/responses/ValidationFailed"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "502":
          $ref: "#/components/responses/StoreUnavailable"
  /api/v1/products/{id}/drops:
    get:
      tags: [Drops]
      operationId: listProductDrops
      summary: List a product's scheduled changes
      description: >
        The drops that will change this product (or its variants), each narrowed
        to the changes that touch it. Bounded by the account's drops, so the
        whole list is returned and next_cursor is always null.
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
      responses:
        "200":
          $ref: "#/components/responses/DropList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/variants/{id}/sales-channels:
    get:
      tags: [Sales channels]
      operationId: listVariantSalesChannels
      summary: List a variant's sales channels on one store
      description: >
        The Shopify sales channels this variant is published to on the named
        store. Reaches Shopify live, so it can answer 502.
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
        - $ref: "#/components/parameters/RequiredStoreIdQuery"
      responses:
        "200":
          $ref: "#/components/responses/SalesChannelList"
        "400":
          $ref: "#/components/responses/ValidationFailed"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "502":
          $ref: "#/components/responses/StoreUnavailable"
  /api/v1/variants/{id}/drops:
    get:
      tags: [Drops]
      operationId: listVariantDrops
      summary: List a variant's scheduled changes
      description: >
        The drops that will change this variant, each narrowed to the changes
        that touch it. next_cursor is always null.
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
      responses:
        "200":
          $ref: "#/components/responses/DropList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/collections/{id}/translations:
    get:
      tags: [Translations]
      operationId: listCollectionTranslations
      summary: List a collection's translations
      description: >
        Every locale this collection is translated into, with the translated
        values per store. next_cursor is always null.
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
      responses:
        "200":
          $ref: "#/components/responses/EntityTranslations"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/collections/{id}/sales-channels:
    get:
      tags: [Sales channels]
      operationId: listCollectionSalesChannels
      summary: List a collection's sales channels on one store
      description: >
        The Shopify sales channels this collection is published to on the named
        store. Reaches Shopify live, so it can answer 502.
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
        - $ref: "#/components/parameters/RequiredStoreIdQuery"
      responses:
        "200":
          $ref: "#/components/responses/SalesChannelList"
        "400":
          $ref: "#/components/responses/ValidationFailed"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "502":
          $ref: "#/components/responses/StoreUnavailable"
  /api/v1/collections/{id}/drops:
    get:
      tags: [Drops]
      operationId: listCollectionDrops
      summary: List a collection's scheduled changes
      description: >
        The drops that will change this collection, each narrowed to the changes
        that touch it. next_cursor is always null.
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
      responses:
        "200":
          $ref: "#/components/responses/DropList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/stores/{id}/sales-channels:
    get:
      tags: [Sales channels]
      operationId: listStoreSalesChannels
      summary: List a store's sales channels
      description: >
        Every Shopify sales channel the store owns — the set an entity's own
        channel membership is drawn from. Reaches Shopify live, so it can
        answer 502. next_cursor is always null.
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/StoreIdPath"
      responses:
        "200":
          $ref: "#/components/responses/SalesChannelList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "502":
          $ref: "#/components/responses/StoreUnavailable"
  /api/v1/markets:
    get:
      tags: [Markets]
      operationId: listMarkets
      summary: List markets
      description: >
        Every Shopify market in the account with its per-store records and the
        catalogs attached to it — the region, currency, and price-list structure
        a pricing or localization integration reads before it writes store-local
        values. Bounded by the account, so the whole list is returned and
        next_cursor is always null.
      x-required-scope: pim:read
      responses:
        "200":
          $ref: "#/components/responses/MarketList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/markets/{id}:
    get:
      tags: [Markets]
      operationId: getMarket
      summary: Get one market
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
      responses:
        "200":
          description: One market.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Market"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/metafield-definitions:
    get:
      tags: [Definitions]
      operationId: listMetafieldDefinitions
      summary: List metafield definitions
      description: >
        The custom fields an entity may carry, and how each one is typed —
        the schema layer to read before writing metafield values. Bounded by the
        account, so the whole list is returned and next_cursor is always null.
      x-required-scope: pim:read
      parameters:
        - name: owner_type
          in: query
          description: Narrow to one owner kind.
          schema:
            type: string
            enum: [product, variant, collection]
      responses:
        "200":
          $ref: "#/components/responses/MetafieldDefinitionList"
        "400":
          $ref: "#/components/responses/ValidationFailed"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/metafield-definitions/{id}:
    get:
      tags: [Definitions]
      operationId: getMetafieldDefinition
      summary: Get one metafield definition
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
      responses:
        "200":
          description: One metafield definition.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetafieldDefinition"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/metaobject-definitions:
    get:
      tags: [Definitions]
      operationId: listMetaobjectDefinitions
      summary: List metaobject definitions
      description: >
        The structured content types the account defines, each with its
        canonical field structure and its account-wide entry count.
        next_cursor is always null.
      x-required-scope: pim:read
      responses:
        "200":
          $ref: "#/components/responses/MetaobjectDefinitionList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/metaobject-definitions/{id}:
    get:
      tags: [Definitions]
      operationId: getMetaobjectDefinition
      summary: Get one metaobject definition
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
      responses:
        "200":
          description: One metaobject definition.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetaobjectDefinition"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/metaobject-definitions/{id}/entries:
    get:
      tags: [Definitions]
      operationId: listMetaobjectEntries
      summary: List a definition's entries
      description: >
        The structured content records under one metaobject definition, with
        their values and per-store publish state.
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
      responses:
        "200":
          $ref: "#/components/responses/MetaobjectEntryList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/metaobject-definitions/{id}/translations:
    get:
      tags: [Translations]
      operationId: listMetaobjectDefinitionTranslations
      summary: List a metaobject definition's translations
      description: >
        The definition's localized labels — its display name and one row per
        field of its canonical structure. next_cursor is always null.
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
      responses:
        "200":
          $ref: "#/components/responses/EntityTranslations"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/drops:
    get:
      tags: [Drops]
      operationId: listDrops
      summary: List drops
      description: >
        Every scheduled change window in the account with its aggregates. The
        per-change payload is on the detail read. next_cursor is always null.
      x-required-scope: pim:read
      responses:
        "200":
          $ref: "#/components/responses/DropList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/drops/{id}:
    get:
      tags: [Drops]
      operationId: getDrop
      summary: Get one drop
      description: >
        One drop with every change it carries, including each change's
        apply/revert status and, when it failed, the reason.
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/EntityId"
      responses:
        "200":
          description: One drop with its changes.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Drop"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/data-health:
    get:
      tags: [Data health]
      operationId: getDataHealth
      summary: Read cached data health
      description: >
        The account's catalog-quality state per dimension, as of the last scan.
        Data health is derived, re-computable state, so this read never scans:
        a dimension that has never been scanned reports scanned=false, which is
        a different answer from "scanned, and clean". Each dimension's `result`
        is an opaque object whose shape is specific to that dimension.
      x-required-scope: pim:read
      parameters:
        - $ref: "#/components/parameters/OptionalStoreIdQuery"
      responses:
        "200":
          description: The account's cached data health.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataHealth"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/data-health/issues:
    get:
      tags: [Data health]
      operationId: listDataHealthIssues
      summary: List data health issues
      description: >
        One page of one section's issue rows — the entities behind a dimension's
        numbers. Page-numbered rather than cursor-paged, because a section is a
        live query over changing state rather than a stable list.
      x-required-scope: pim:read
      parameters:
        - name: section
          in: query
          required: true
          description: Which issue list to read.
          schema:
            type: string
        - $ref: "#/components/parameters/OptionalStoreIdQuery"
        - name: locale
          in: query
          description: Narrow the translation sections to one target language.
          schema:
            type: string
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: page_size
          in: query
          description: Clamped to 100.
          schema:
            type: integer
            minimum: 1
            default: 25
            maximum: 100
      responses:
        "200":
          description: One page of issues.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataHealthIssues"
        "400":
          $ref: "#/components/responses/ValidationFailed"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/data-health/scans:
    post:
      tags: [Data health]
      operationId: createDataHealthScan
      summary: Run one data health scan
      description: >
        Recompute one dimension and cache the result. This is the expensive
        path — it scans the catalog — so it is a request, never something to
        poll. It touches no merchant data.
      x-required-scope: pim:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                dimension:
                  type: string
                  description: Which dimension to recompute.
                store_id:
                  type: string
                  format: uuid
                  description: Scope the scan to one store, where the dimension is store-scoped.
              required: [dimension]
      responses:
        "200":
          description: The fresh snapshot.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataHealthDimensionState"
        "400":
          $ref: "#/components/responses/ValidationFailed"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/data-health/completeness-fills:
    post:
      tags: [Data health]
      operationId: createCompletenessFill
      summary: Fill empty fields
      description: >
        Write authored values into fields that are still empty. A field that has
        been filled since the scan is skipped, never overwritten, so a stale
        batch cannot clobber newer values. Writes drafts only — nothing reaches
        Shopify, and publishing stays an explicit follow-up. Partial success is
        normal: read `results` per operation, not just the status code.
      x-required-scope: pim:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                entity_type:
                  type: string
                operations:
                  type: array
                  maxItems: 500
                  items:
                    type: object
                    properties:
                      entity_id:
                        type: string
                        format: uuid
                      store_id:
                        type: string
                        format: uuid
                      field:
                        type: string
                      value:
                        type: string
                    required: [entity_id, store_id, field, value]
              required: [entity_type, operations]
      responses:
        "200":
          description: The batch outcome, per operation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CompletenessFillResult"
        "400":
          $ref: "#/components/responses/ValidationFailed"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "423":
          $ref: "#/components/responses/PIMLocked"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/data-health/divergence-reconciles:
    post:
      tags: [Data health]
      operationId: createDivergenceReconcile
      summary: Harmonize diverging store values
      description: >
        Bring one field into agreement across stores, either by adopting a
        winner store's value or by writing an explicit one. Stores that already
        match are left untouched. Writes drafts only. Partial success is normal:
        read `results` per operation.
      x-required-scope: pim:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                entity_type:
                  type: string
                operations:
                  type: array
                  items:
                    type: object
                    properties:
                      entity_id:
                        type: string
                        format: uuid
                      field:
                        type: string
                      winner_store_id:
                        type: string
                        format: uuid
                        description: >
                          Adopt this store's value. Mutually exclusive with
                          `value`.
                      value:
                        type: [string, "null"]
                        description: >
                          Write this value to every store that differs, or to
                          `store_id` alone when it is set. Mutually exclusive
                          with `winner_store_id`.
                      store_id:
                        type: string
                        format: uuid
                        description: Narrow an explicit `value` to one store.
                    required: [entity_id, field]
              required: [entity_type, operations]
      responses:
        "200":
          description: The batch outcome, per operation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DivergenceReconcileResult"
        "400":
          $ref: "#/components/responses/ValidationFailed"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "423":
          $ref: "#/components/responses/PIMLocked"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/data-health/unused-deletions:
    post:
      tags: [Data health]
      operationId: createUnusedDeletion
      summary: Delete unused entities (reserved)
      description: >
        DELETES entities across every store they live on. Every candidate is
        re-verified as still-unused immediately before deletion, so an entity
        that came back into use since the scan is skipped rather than removed.

        Reserved, not reachable: this route requires `pim:delete`, and
        `pim:delete` is not granted by the consent flow or by key minting, so it
        answers 403 for every credential that exists today. It is documented so
        the contract is honest about what the surface would do if the scope were
        ever granted.
      x-required-scope: pim:delete
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                section:
                  type: string
                  description: Which unused list to delete from.
                store_id:
                  type: string
                  format: uuid
                entity_ids:
                  type: array
                  maxItems: 100
                  items:
                    type: string
                    format: uuid
                  description: Explicit selection. Mutually exclusive with `all`.
                exclude_entity_ids:
                  type: array
                  maxItems: 500
                  items:
                    type: string
                    format: uuid
                  description: >
                    Delete-all mode only: ids earlier batches failed on, so a
                    persistent failure cannot stall the run.
                all:
                  type: boolean
                  description: >
                    Delete one batch of every deletable candidate. The response's
                    `remaining` drives the caller's loop.
              required: [section]
      responses:
        "200":
          description: The batch outcome, per entity.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UnusedDeleteResult"
        "400":
          $ref: "#/components/responses/ValidationFailed"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "423":
          $ref: "#/components/responses/PIMLocked"
        "429":
          $ref: "#/components/responses/RateLimited"
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: >
        An API key created in Settings -> API, sent as
        `Authorization: Bearer pk_live_...`. Keys are stored hashed and looked
        up on every request, so revoking one takes effect immediately.
  headers:
    XRateLimitLimit:
      description: Requests allowed for this key in the current minute.
      schema:
        type: integer
    XRateLimitRemaining:
      description: Requests left in the current minute.
      schema:
        type: integer
    XRateLimitReset:
      description: Unix second the current minute window resets.
      schema:
        type: integer
    XRequestId:
      description: Correlation id for this request; quote it in support requests.
      schema:
        type: string
  parameters:
    Limit:
      name: limit
      in: query
      required: false
      description: >
        Rows per page. Defaults to 50, maximum 250; a value above the maximum
        is clamped and a non-numeric value falls back to the default.
      schema:
        type: integer
        minimum: 1
        maximum: 250
        default: 50
    Cursor:
      name: cursor
      in: query
      required: false
      description: >
        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` rather than silently skipping rows.
      schema:
        type: string
    UpdatedSince:
      name: updated_since
      in: query
      required: false
      description: >
        Return only entities whose own record changed at or after this RFC3339
        timestamp. "Own record" means the entity's canonical, cross-store data
        — its shared values and its structure. Editing one store's version of
        an entity does not widen this filter, so a poller that must catch
        store-local edits should re-read the store versions of the entities it
        tracks. A non-RFC3339 value is rejected with `validation_failed`.
      schema:
        type: string
        format: date-time
    Status:
      name: status
      in: query
      required: false
      description: >
        Return only entities in this publish state. This is where the entity
        stands with Peak PIM's stores, not Shopify's own product status (which
        is a per-store attribute value).
      schema:
        type: string
        enum: [never_published, pending, published, failed]
    Search:
      name: search
      in: query
      required: false
      description: >
        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 name/title/vendor/tags; collections by name or store title;
        media files by filename, tags, or alt text.
      schema:
        type: string
    EntityId:
      name: id
      in: path
      required: true
      description: The entity's id.
      schema:
        type: string
        format: uuid
    StoreId:
      name: store_id
      in: path
      required: true
      description: The store's id, as returned by GET /api/v1/stores.
      schema:
        type: string
        format: uuid
    StoreIdPath:
      name: id
      in: path
      required: true
      description: The store's id, as returned by GET /api/v1/stores.
      schema:
        type: string
        format: uuid
    RequiredStoreIdQuery:
      name: store_id
      in: query
      required: true
      description: >
        Which store to answer for. Sales-channel membership is per store, so
        there is no cross-store answer to give.
      schema:
        type: string
        format: uuid
    OptionalStoreIdQuery:
      name: store_id
      in: query
      required: false
      description: >
        Scope to one store. Omitted, the answer is account-wide; a store that is
        not this account's is reported as not found.
      schema:
        type: string
        format: uuid
  responses:
    EntityListPage:
      description: One page of entities.
      headers:
        X-RateLimit-Limit:
          $ref: "#/components/headers/XRateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/XRateLimitRemaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/XRateLimitReset"
        X-Request-Id:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            type: object
            properties:
              data:
                type: array
                items:
                  $ref: "#/components/schemas/EntityListItem"
              next_cursor:
                type: [string, "null"]
                description: >
                  Pass it back as `cursor` for the next page. Null on the last
                  page.
            required: [data, next_cursor]
    Entity:
      description: One entity.
      headers:
        X-RateLimit-Limit:
          $ref: "#/components/headers/XRateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/XRateLimitRemaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/XRateLimitReset"
        X-Request-Id:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Entity"
    StoreVersionList:
      description: Every store version of one entity. next_cursor is always null.
      headers:
        X-RateLimit-Limit:
          $ref: "#/components/headers/XRateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/XRateLimitRemaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/XRateLimitReset"
        X-Request-Id:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            type: object
            properties:
              data:
                type: array
                items:
                  $ref: "#/components/schemas/StoreVersion"
              next_cursor:
                type: "null"
            required: [data, next_cursor]
    StoreVersion:
      description: One store's version of one entity.
      headers:
        X-RateLimit-Limit:
          $ref: "#/components/headers/XRateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/XRateLimitRemaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/XRateLimitReset"
        X-Request-Id:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/StoreVersion"
    ValidationFailed:
      description: A parameter was understood but rejected.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: No credential, or one that no longer resolves.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: >
        The credential has no authority here — a session credential on the
        public API, an API key outside it, or a key without the route's scope
        (`forbidden_scope`).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: >
        No such record in this account. A record that belongs to another
        account, an id of a different kind of entity, and an entity with no
        version on the requested store are all reported the same way.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    RateLimited:
      description: Over the per-key or per-account minute budget.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
        X-RateLimit-Limit:
          $ref: "#/components/headers/XRateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/XRateLimitRemaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/XRateLimitReset"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    PIMLocked:
      description: >
        An import or store refresh is reconciling, so writes are held until the
        merchant finishes the review.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    StoreUnavailable:
      description: >
        The store could not be reached. Sales-channel reads go to Shopify live,
        so they depend on Shopify's availability rather than ours.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    MarketList:
      description: The account's markets. next_cursor is always null.
      content:
        application/json:
          schema:
            type: object
            properties:
              data:
                type: array
                items:
                  $ref: "#/components/schemas/Market"
              next_cursor:
                type: "null"
            required: [data, next_cursor]
    MetafieldDefinitionList:
      description: The account's metafield definitions. next_cursor is always null.
      content:
        application/json:
          schema:
            type: object
            properties:
              data:
                type: array
                items:
                  $ref: "#/components/schemas/MetafieldDefinition"
              next_cursor:
                type: "null"
            required: [data, next_cursor]
    MetaobjectDefinitionList:
      description: The account's metaobject definitions. next_cursor is always null.
      content:
        application/json:
          schema:
            type: object
            properties:
              data:
                type: array
                items:
                  $ref: "#/components/schemas/MetaobjectDefinition"
              next_cursor:
                type: "null"
            required: [data, next_cursor]
    MetaobjectEntryList:
      description: One definition's entries. next_cursor is always null.
      content:
        application/json:
          schema:
            type: object
            properties:
              data:
                type: array
                items:
                  $ref: "#/components/schemas/MetaobjectEntry"
              next_cursor:
                type: "null"
            required: [data, next_cursor]
    SalesChannelList:
      description: Sales channels. next_cursor is always null.
      content:
        application/json:
          schema:
            type: object
            properties:
              data:
                type: array
                items:
                  $ref: "#/components/schemas/SalesChannel"
              next_cursor:
                type: "null"
            required: [data, next_cursor]
    DropList:
      description: Drops. next_cursor is always null.
      content:
        application/json:
          schema:
            type: object
            properties:
              data:
                type: array
                items:
                  $ref: "#/components/schemas/Drop"
              next_cursor:
                type: "null"
            required: [data, next_cursor]
    EntityTranslations:
      description: One entity's translations, grouped by locale then store.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/EntityTranslations"
  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              $ref: "#/components/schemas/ErrorCode"
            message:
              type: string
              description: Human-readable explanation. Never branch on it.
            request_id:
              type: string
          required: [code, message]
      required: [error]
    ErrorCode:
      type: string
      description: >
        The published error-code set. It only ever grows, so treat an unknown
        code as a generic failure of its HTTP status.
      enum:
        - unauthorized
        - forbidden
        - forbidden_scope
        - not_found
        - validation_failed
        - pim_locked
        - rate_limited
        - conflict
        - internal_error
    EntityType:
      type: string
      enum: [product, variant, collection, media]
    PublishStatus:
      type: string
      description: >
        Where the draft stands with the store(s): never published, changes
        pending, published successfully, or the last publish failed. The
        values are identical to the `status` list filter's, so a response
        value can be fed straight back into the filter.
      enum: [never_published, pending, published, failed]
    Attribute:
      type: object
      description: One field of an entity or of one of its store versions.
      properties:
        key:
          type: string
        value:
          type: string
        type:
          type: string
      required: [key, value, type]
    EntityListItem:
      type: object
      properties:
        id:
          type: string
          format: uuid
        type:
          $ref: "#/components/schemas/EntityType"
        title:
          type: string
        draft_revision:
          type: integer
          description: >
            The entity's revision. It is the optimistic-concurrency handle a
            later write sends back to prove it read the current state.
        pending_publish:
          type: boolean
        last_publish_status:
          $ref: "#/components/schemas/PublishStatus"
        store_ids:
          type: array
          items:
            type: string
            format: uuid
      required:
        - id
        - type
        - title
        - draft_revision
        - pending_publish
        - last_publish_status
        - store_ids
    Entity:
      type: object
      properties:
        id:
          type: string
          format: uuid
        type:
          $ref: "#/components/schemas/EntityType"
        title:
          type: string
        attributes:
          type: array
          description: The canonical values every store inherits.
          items:
            $ref: "#/components/schemas/Attribute"
        draft_revision:
          type: integer
        pending_publish:
          type: boolean
        last_publish_status:
          $ref: "#/components/schemas/PublishStatus"
        product_id:
          type: string
          format: uuid
          description: The product a variant belongs to. Variants only.
        variant_ids:
          type: array
          description: The product's variants. Products only.
          items:
            type: string
            format: uuid
        media_ids:
          type: array
          description: >
            The media files this entity uses, in gallery order, across every
            store. Absent on media files themselves.
          items:
            type: string
            format: uuid
        stores:
          type: array
          description: >
            Publish state per store the entity lives on. The store's values are
            on the store subresource.
          items:
            $ref: "#/components/schemas/EntityStoreState"
      required:
        - id
        - type
        - title
        - attributes
        - draft_revision
        - pending_publish
        - last_publish_status
        - stores
    EntityStoreState:
      type: object
      properties:
        store_id:
          type: string
          format: uuid
        store_domain:
          type: string
        pending_publish:
          type: boolean
        last_publish_status:
          $ref: "#/components/schemas/PublishStatus"
      required: [store_id, store_domain, pending_publish, last_publish_status]
    StoreVersion:
      type: object
      description: >
        One store's version of an entity: the values that store publishes,
        which may differ from every other store's.
      properties:
        store_id:
          type: string
          format: uuid
        store_domain:
          type: string
        attributes:
          type: array
          items:
            $ref: "#/components/schemas/Attribute"
        pending_publish:
          type: boolean
        last_publish_status:
          $ref: "#/components/schemas/PublishStatus"
      required:
        - store_id
        - store_domain
        - attributes
        - pending_publish
        - last_publish_status
    Store:
      type: object
      properties:
        id:
          type: string
          format: uuid
        domain:
          type: string
          description: The Shopify domain, e.g. acme.myshopify.com.
        name:
          type: string
      required: [id, domain, name]
    Market:
      type: object
      description: >
        One Shopify market with its per-store records and the catalogs attached
        to it.
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        pending_publish:
          type: boolean
          description: The market has unpublished changes.
        stores:
          type: array
          items:
            $ref: "#/components/schemas/MarketStoreRecord"
        catalogs:
          type: array
          items:
            $ref: "#/components/schemas/Catalog"
      required: [id, name, pending_publish, stores, catalogs]
    MarketStoreRecord:
      type: object
      description: One store's version of a market.
      properties:
        store_id:
          type: string
          format: uuid
        store_domain:
          type: string
        shopify_gid:
          type: string
        name:
          type: string
        regions:
          type: array
          items:
            type: string
          description: The country/region codes the market covers.
        currency:
          type: string
        web_presences:
          description: The market's web presences, as Shopify returned them.
        pending_publish:
          type: boolean
        last_publish_status:
          $ref: "#/components/schemas/PublishStatus"
      required: [store_id, store_domain, regions, pending_publish, last_publish_status]
    Catalog:
      type: object
      description: One catalog attached to a market.
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        slug:
          type: string
        pending_publish:
          type: boolean
        stores:
          type: array
          items:
            $ref: "#/components/schemas/CatalogStoreRecord"
      required: [id, title, pending_publish, stores]
    CatalogStoreRecord:
      type: object
      description: >
        One store's version of a catalog, with the price-list and publication
        handles a pricing integration addresses Shopify by.
      properties:
        store_id:
          type: string
          format: uuid
        store_domain:
          type: string
        shopify_gid:
          type: string
        status:
          type: string
        currency:
          type: string
        price_adjustment:
          type: string
        auto_publish:
          type: boolean
        price_list_gid:
          type: string
        publication_gid:
          type: string
        pending_publish:
          type: boolean
        last_publish_status:
          $ref: "#/components/schemas/PublishStatus"
      required: [store_id, store_domain, auto_publish, pending_publish, last_publish_status]
    MetafieldDefinition:
      type: object
      description: >
        One custom field an entity may carry, and how it is typed. Read this
        before writing metafield values.
      properties:
        id:
          type: string
          format: uuid
        namespace:
          type: string
        key:
          type: string
        name:
          type: string
        description:
          type: string
        type:
          type: string
          description: Shopify's field type, e.g. single_line_text_field.
        owner_type:
          type: string
          description: Which kind of entity carries this field.
        validations:
          type: array
          items:
            $ref: "#/components/schemas/MetafieldValidation"
        position:
          type: [integer, "null"]
          description: >
            The merchant's manual display position, or null when the definition
            has never been reordered.
        pending_publish:
          type: boolean
        last_publish_status:
          $ref: "#/components/schemas/PublishStatus"
        stores:
          type: array
          items:
            $ref: "#/components/schemas/DefinitionStorePresence"
      required: [id, namespace, key, name, type, owner_type, position, pending_publish, last_publish_status, stores]
    MetafieldValidation:
      type: object
      properties:
        name:
          type: string
        value:
          type: string
      required: [name, value]
    DefinitionStorePresence:
      type: object
      description: One store a definition exists on.
      properties:
        store_id:
          type: string
          format: uuid
        store_domain:
          type: string
        shopify_gid:
          type: string
        name:
          type: string
        description:
          type: string
        pending_publish:
          type: boolean
      required: [store_id, store_domain, pending_publish]
    MetaobjectDefinition:
      type: object
      description: >
        One structured content type, with its canonical field structure and its
        account-wide entry count.
      properties:
        id:
          type: string
          format: uuid
        type:
          type: string
          description: The Shopify definition type — the cross-store identity key.
        name:
          type: string
        description:
          type: string
        display_name_key:
          type: string
          description: Which field's value names an entry.
        fields:
          type: array
          items:
            $ref: "#/components/schemas/MetaobjectField"
        publishable:
          type: boolean
          description: Entries can be draft or active.
        entry_count:
          type: integer
        has_structure_conflict:
          type: boolean
          description: >
            Stores disagreed about this definition's structure as of the last
            import.
        pending_publish:
          type: boolean
        last_publish_status:
          $ref: "#/components/schemas/PublishStatus"
        stores:
          type: array
          items:
            $ref: "#/components/schemas/DefinitionStorePresence"
      required: [id, type, name, fields, publishable, entry_count, has_structure_conflict, pending_publish, last_publish_status, stores]
    MetaobjectField:
      type: object
      description: One field of a metaobject definition, in definition order.
      properties:
        key:
          type: string
        name:
          type: string
        description:
          type: string
        type:
          type: string
        required:
          type: boolean
        validations:
          type: array
          items:
            $ref: "#/components/schemas/MetafieldValidation"
      required: [key, name, type, required]
    MetaobjectEntry:
      type: object
      description: One structured content record under a metaobject definition.
      properties:
        id:
          type: string
          format: uuid
        definition_id:
          type: string
          format: uuid
        handle:
          type: string
        title:
          type: string
        attributes:
          type: array
          items:
            $ref: "#/components/schemas/Attribute"
        draft_revision:
          type: integer
        pending_publish:
          type: boolean
        last_publish_status:
          $ref: "#/components/schemas/PublishStatus"
        stores:
          type: array
          items:
            $ref: "#/components/schemas/EntityStoreState"
      required: [id, definition_id, title, attributes, draft_revision, pending_publish, last_publish_status, stores]
    EntityTranslations:
      type: object
      description: >
        One entity's translations: the fields that can carry one, and the
        translated values per locale per store.
      properties:
        entity_id:
          type: string
          format: uuid
        entity_type:
          type: string
        translatable_fields:
          type: array
          items:
            $ref: "#/components/schemas/TranslatableField"
        locales:
          type: array
          items:
            $ref: "#/components/schemas/LocaleTranslation"
      required: [entity_id, entity_type, translatable_fields, locales]
    TranslatableField:
      type: object
      properties:
        key:
          type: string
          description: The field key a translated value is written under.
        shopify_key:
          type: string
          description: The same field as Shopify names it.
        label:
          type: string
        reference:
          type: string
          description: The source value this row translates, when it is not on the entity.
      required: [key]
    LocaleTranslation:
      type: object
      properties:
        locale:
          type: string
          description: BCP-47 language code.
        stores:
          type: array
          items:
            $ref: "#/components/schemas/LocaleStoreTranslation"
      required: [locale, stores]
    LocaleStoreTranslation:
      type: object
      properties:
        store_id:
          type: string
          format: uuid
        store_domain:
          type: string
        values:
          type: array
          items:
            $ref: "#/components/schemas/Attribute"
        pending_publish:
          type: boolean
        last_publish_status:
          $ref: "#/components/schemas/PublishStatus"
      required: [store_id, store_domain, values, pending_publish, last_publish_status]
    SalesChannel:
      type: object
      description: One Shopify sales channel (publication).
      properties:
        id:
          type: string
          description: The Shopify publication GID.
        name:
          type: string
      required: [id, name]
    Drop:
      type: object
      description: >
        One scheduled change window: a named period that applies values when it
        opens and reverts them when it closes.
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        state:
          type: string
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        next_fire_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        last_error:
          type: string
        change_count:
          type: integer
        entity_count:
          type: integer
        failed_change_count:
          type: integer
        changes:
          type: array
          description: Present on the detail read; omitted on list rows.
          items:
            $ref: "#/components/schemas/DropChange"
      required: [id, name, state, starts_at, change_count, entity_count, failed_change_count]
    DropChange:
      type: object
      description: One field change on one entity, on one store.
      properties:
        id:
          type: string
          format: uuid
        entity_id:
          type: string
          format: uuid
        store_id:
          type: string
          format: uuid
        store_domain:
          type: string
        field_key:
          type: string
        target_value:
          type: string
          description: The value the drop will apply.
        captured_value:
          type: string
          description: The value captured before applying, restored on revert.
        captured_at:
          type: string
          format: date-time
        apply_status:
          type: string
        apply_error:
          type: string
        revert_status:
          type: string
        revert_error:
          type: string
      required: [id, entity_id, store_id, field_key, target_value, captured_value, apply_status, revert_status]
    DataHealth:
      type: object
      description: The account's catalog-quality state, as of the last scan.
      properties:
        store_id:
          type: string
          format: uuid
          description: Set when the read was scoped to one store.
        dimensions:
          type: array
          items:
            $ref: "#/components/schemas/DataHealthDimensionState"
      required: [dimensions]
    DataHealthDimensionState:
      type: object
      description: >
        One dimension's cached result, or the fact that it has never been
        scanned. `result` is an opaque object whose shape is specific to the
        dimension and may gain fields at any time.
      properties:
        dimension:
          type: string
        scanned:
          type: boolean
          description: >
            False means no scan has ever run for this dimension — a different
            answer from "scanned, and clean".
        result:
          description: The dimension's summary. Absent when scanned is false.
        scanned_at:
          type: string
          format: date-time
          description: Absent when scanned is false.
      required: [dimension, scanned]
    DataHealthIssues:
      type: object
      description: One page of one section's issue rows.
      properties:
        section:
          type: string
        data:
          type: array
          items:
            $ref: "#/components/schemas/DataHealthIssue"
        page:
          type: integer
        has_next:
          type: boolean
        compared:
          type: integer
          description: >
            Divergence section only: how many multi-store entities the scan
            compared to produce this list.
      required: [section, data, page, has_next]
    DataHealthIssue:
      type: object
      description: One entity behind a dimension's numbers.
      properties:
        entity_id:
          type: string
          format: uuid
        entity_type:
          type: string
        title:
          type: string
        store_id:
          type: string
          format: uuid
          description: Set on store-scoped sections.
        detail:
          type: string
        fields:
          type: array
          items:
            type: string
          description: The fields the issue is about.
        locale:
          type: string
          description: Translation sections only.
        missing_store_ids:
          type: array
          items:
            type: string
            format: uuid
          description: Coverage sections only — stores with no version at all.
        never_published_store_ids:
          type: array
          items:
            type: string
            format: uuid
          description: Coverage sections only — stores that were never published to.
      required: [entity_id, entity_type, title]
    CompletenessFillResult:
      type: object
      description: >
        One fill batch's outcome. Partial success is normal — read `results`, not
        just the status code.
      properties:
        filled:
          type: integer
          description: How many values were actually written.
        results:
          type: array
          items:
            $ref: "#/components/schemas/CompletenessFillOutcome"
      required: [filled, results]
    CompletenessFillOutcome:
      type: object
      properties:
        entity_id:
          type: string
          format: uuid
        store_id:
          type: string
          format: uuid
        field:
          type: string
        ok:
          type: boolean
          description: The gap is settled.
        filled:
          type: boolean
          description: >
            A value was actually written. ok true with filled false means
            someone else filled the field first.
        error:
          type: string
      required: [entity_id, store_id, field, ok, filled]
    DivergenceReconcileResult:
      type: object
      description: One harmonization batch's outcome.
      properties:
        patched:
          type: integer
          description: How many store versions were actually written.
        results:
          type: array
          items:
            $ref: "#/components/schemas/DivergenceReconcileOutcome"
      required: [patched, results]
    DivergenceReconcileOutcome:
      type: object
      properties:
        entity_id:
          type: string
          format: uuid
        store_id:
          type: string
          format: uuid
        ok:
          type: boolean
        error:
          type: string
      required: [entity_id, store_id, ok]
    UnusedDeleteResult:
      type: object
      description: One deletion batch's outcome.
      properties:
        section:
          type: string
        deleted:
          type: integer
        skipped:
          type: integer
        remaining:
          type: integer
          description: >
            Delete-all mode: deletable candidates still in scope, so a caller
            knows whether to send another batch.
        results:
          type: array
          items:
            $ref: "#/components/schemas/UnusedDeleteOutcome"
      required: [section, deleted, skipped, remaining, results]
    UnusedDeleteOutcome:
      type: object
      properties:
        entity_id:
          type: string
          format: uuid
        title:
          type: string
        ok:
          type: boolean
        message:
          type: string
          description: Why the entity was skipped, when ok is false.
      required: [entity_id, ok]
