> ## Documentation Index
> Fetch the complete documentation index at: https://docs.depict.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Headless and native app integration

> Read Depict's merchandising data straight from the Shopify Storefront API, for native mobile apps and custom storefronts.

<Note>
  This page is aimed at a merchant's app developers: the people building a
  native mobile app or a custom (headless) storefront that should show the
  same merchandised collection pages as the web storefront.
</Note>

The Depict app publishes everything it produces into your own Shopify store:

* The **product order** of each collection is written to the collection
  itself as a manual sort order. A plain Storefront API query for the
  collection's products, in default order, returns the merchandised order.
* **Content blocks** (banners, images, videos, text tiles), **image
  settings** (per-collection product image overrides), **product duplicates**
  (a product shown a second time with its own imagery) and **per-market
  product orders** are stored as Shopify metaobjects, referenced from
  metafields, all with storefront access `PUBLIC_READ`.

There is no Depict API in the read path. Everything below is served by
Shopify, with Shopify's availability, caching and CDN. Depict resolves
drafts, versions and scheduled publishes server side at publish time, so the
references you read through the paths on this page always point at the
published state.

For how to lay the data out on screen, see the
[collection grid rendering specification](/shopify-lite/headless/grid-rendering-spec).

## Prerequisites

* A **Storefront API access token** for the shop. Any token works; the data
  is public read. Use a recent API version (the examples use `2026-07`).

## Namespaces

Depict creates its metafield definitions under Shopify's app-reserved
namespaces (`$app:content-blocks` and so on). To every other API client,
including your storefront token, an app-reserved namespace appears in the
concrete form `app--{app-id}--{namespace}`. The Depict app's ID is
`93504864257` on every store, so the namespaces are:

| Owner      | Namespace                             | Key                 | Type                        |
| ---------- | ------------------------------------- | ------------------- | --------------------------- |
| Collection | `app--93504864257--content-blocks`    | `content_block`     | `list.metaobject_reference` |
| Collection | `app--93504864257--image-setting`     | `image_setting`     | `list.metaobject_reference` |
| Collection | `app--93504864257--product-duplicate` | `product_duplicate` | `list.metaobject_reference` |
| Shop       | `app--93504864257--column-settings`   | `column_settings`   | `list.number_integer`       |
| Shop       | `app--93504864257--grid-settings`     | `grid_settings`     | `json`                      |

Metaobject **types** (`depict_content_block`, `depict_image_setting`,
`depict_product_duplicate`, `depict_market_products`,
`depict_multi_market_collection`) are not app-prefixed and are the same on
every store.

You may also come across sibling metafields whose namespaces end in
`-versions` (for example `app--93504864257--content-blocks-versions`). They
hold Depict's internal draft and version bookkeeping, which includes
unpublished state. Ignore them; the paths on this page only ever reference
the published state.

## The data model at a glance

| Piece                   | Where it lives                                                                                                        | What it does                                                                                    |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| Product order (default) | The collection's own manual sort order                                                                                | Merchandised order for the default market                                                       |
| Content blocks          | Collection metafield `content_block` referencing `depict_content_block` metaobjects                                   | Media tiles placed into the product grid                                                        |
| Image settings          | Collection metafield `image_setting` referencing `depict_image_setting` metaobjects                                   | Override a product's main and hover image within this collection                                |
| Product duplicates      | Collection metafield `product_duplicate` referencing `depict_product_duplicate` metaobjects                           | Show a product an extra time with its own imagery                                               |
| Per-market overrides    | `depict_multi_market_collection` metaobject per (collection, market)                                                  | Market-specific order, blocks, duplicates and image settings                                    |
| Column settings         | Shop metafield `column_settings`                                                                                      | Configured grid column counts: `[desktop, mobile]`                                              |
| Grid settings           | Shop metafield `grid_settings`                                                                                        | Spacing, margins and web-renderer options                                                       |
| A/B tests               | Collection metafield `ab_test_v3` (namespace `app--93504864257--ab-test`) referencing `depict_ab_test_v3` metaobjects | Active collection test windows, see [Matching the web storefront](#matching-the-web-storefront) |

## Product order

The Depict app sets the collection's sort order to manual and writes the
merchandised order into the collection with the Admin API. That means the
default Storefront API product listing is already the merchandised order, no
extra work needed:

```graphql theme={null}
query CollectionProducts($handle: String!, $after: String)
@inContext(country: SE) {
  collection(handle: $handle) {
    id
    title
    products(first: 250, after: $after) {
      nodes {
        id
        handle
        title
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
}
```

Notes:

* Do **not** pass a `sortKey`. The default sort is the collection's own
  order, which is the merchandised order.
* Page through with `after` until `hasNextPage` is false; 250 is the maximum
  page size.
* `@inContext(country: ...)` scopes prices and product availability to the
  buyer's market. It does not change the ordering. Market-specific
  *orderings* come from the per-market metaobjects described
  [below](#markets).

## Content blocks

Content blocks are `depict_content_block` metaobjects, referenced in display
order metadata from the collection metafield. Each block carries placement
fields (where it sits in the grid) and content fields (what it shows).

```graphql theme={null}
query CollectionContentBlocks($handle: String!)
@inContext(country: SE, language: SV) {
  collection(handle: $handle) {
    id
    metafield(namespace: "app--93504864257--content-blocks", key: "content_block") {
      references(first: 250) {
        pageInfo {
          hasNextPage
          endCursor
        }
        nodes {
          ... on Metaobject {
            id
            index: field(key: "index") { value }
            spanColumns: field(key: "span_columns") { value }
            spanRows: field(key: "span_rows") { value }
            aspectRatio: field(key: "aspect_ratio") { value }
            visibility: field(key: "visibility") { value }
            altText: field(key: "alt_text") { value }
            text: field(key: "text") { value }
            space: field(key: "space") { value }
            image: field(key: "image") {
              reference {
                ... on MediaImage {
                  image { url width height altText }
                }
              }
            }
            hoverImage: field(key: "hover_image") {
              reference {
                ... on MediaImage {
                  image { url width height }
                }
              }
            }
            video: field(key: "video") {
              reference {
                ... on Video {
                  sources { url mimeType format width height }
                  previewImage { url }
                }
              }
            }
            linkToCollection: field(key: "link_to_collection") {
              reference { ... on Collection { handle } }
            }
            linkToProduct: field(key: "link_to_product") {
              reference { ... on Product { handle } }
            }
            linkToPage: field(key: "link_to_page") {
              reference { ... on Page { handle } }
            }
            blogHandle: field(key: "blog_handle") { value }
            blogArticleId: field(key: "blog_article_id") { value }
            externalUrl: field(key: "external_url") { value }
            instagram: field(key: "instagram_post_metadata") { value }
          }
        }
      }
    }
  }
}
```

An illustrative response for a collection with one image block:

```json theme={null}
{
  "data": {
    "collection": {
      "id": "gid://shopify/Collection/402462310713",
      "metafield": {
        "references": {
          "pageInfo": { "hasNextPage": false, "endCursor": "eyJsYXN0X2lkIjo4NDUxMjM2MTc4NX0=" },
          "nodes": [
            {
              "id": "gid://shopify/Metaobject/84512361785",
              "index": { "value": "2" },
              "spanColumns": { "value": "2" },
              "spanRows": { "value": "1" },
              "aspectRatio": { "value": "1.5" },
              "visibility": { "value": "" },
              "altText": { "value": "Summer campaign" },
              "text": {
                "value": "{\"header\":{\"html_tag\":\"h2\",\"color_hex\":\"#FFFFFF\",\"bold\":true,\"text\":\"New in\"},\"body\":null,\"horizontal_alignment\":\"center\",\"vertical_alignment\":\"end\",\"background_overlay\":\"rgba(0,0,0,0.35)\",\"overlay_style\":\"gradient\",\"text_shadow\":true,\"gap\":\"4px\"}"
              },
              "space": null,
              "image": {
                "reference": {
                  "image": {
                    "url": "https://cdn.shopify.com/s/files/1/0000/0000/files/campaign.jpg?v=1717000000",
                    "width": 3000,
                    "height": 2000,
                    "altText": null
                  }
                }
              },
              "hoverImage": null,
              "video": null,
              "linkToCollection": { "reference": { "handle": "new-in" } },
              "linkToProduct": null,
              "linkToPage": null,
              "blogHandle": { "value": "" },
              "blogArticleId": { "value": "" },
              "externalUrl": { "value": "" },
              "instagram": null
            }
          ]
        }
      }
    }
  }
}
```

<Note>
  A list metafield can reference more entries than one request returns:
  real stores exceed 100 image settings or duplicates on a single
  collection. `first` is capped at 250, so page through `references` with
  `after: endCursor` until `hasNextPage` is false, exactly like the
  products connection. This applies to the content block, image setting
  and product duplicate queries alike.
</Note>

### Block fields

| Field                                                     | Type           | Meaning                                                                                                                                                         |
| --------------------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `index`                                                   | integer        | Grid slot the block wants, counted in grid cells, row by row from 0. See the [rendering spec](/shopify-lite/headless/grid-rendering-spec).                      |
| `span_columns`                                            | integer        | Width in columns.                                                                                                                                               |
| `span_rows`                                               | integer        | Height in product rows.                                                                                                                                         |
| `aspect_ratio`                                            | decimal        | Width divided by height of the media. Used to size blocks that span the full grid width.                                                                        |
| `visibility`                                              | string         | `"desktop"`, `"mobile"`, or empty for both.                                                                                                                     |
| `image` / `hover_image`                                   | file reference | Images, served from the Shopify Files CDN. `hover_image` is shown on hover on desktop and as a second swipeable frame on touch devices.                         |
| `video`                                                   | file reference | A video instead of an image. `sources` contains MP4 renditions and usually an HLS (`m3u8`) playlist; `previewImage` is the poster.                              |
| `alt_text`                                                | string         | Alt text for the image.                                                                                                                                         |
| `text`                                                    | JSON           | Text overlay, see below.                                                                                                                                        |
| `space`                                                   | JSON           | Marks a spacer block: `{"type": "space", "height": 240}`. `height` is in pixels and optional. Spacers render as empty area (optionally with a text overlay).    |
| `link_to_collection` / `link_to_product` / `link_to_page` | reference      | Tap target. At most one link field is set.                                                                                                                      |
| `blog_handle` + `blog_article_id`                         | strings        | Link to a blog article. `blog_article_id` is a full article GID.                                                                                                |
| `external_url`                                            | string         | Link to an arbitrary URL.                                                                                                                                       |
| `instagram_post_metadata`                                 | JSON           | `{"creator", "post_url", "post_id", "media_id"}`. When present, the web renderer shows a small attribution overlay with the creator name linking to `post_url`. |
| `hover_instagram_post_metadata`                           | JSON           | Same shape, for an Instagram-sourced `hover_image`. Part of the data model; the web renderer does not render it today.                                          |

Fields the merchandiser left unset read as **empty strings** (the app
writes empty strings to clear previous values) or, on blocks created
before a field was added to the definition, come back as null or are
absent. Treat empty, null and absent identically: a field counts as set
only when it has a non-empty value.

The `text` JSON has this shape (all fields optional unless noted):

```json theme={null}
{
  "header": { "html_tag": "h2", "color_hex": "#FFFFFF", "bold": true, "italic": false, "underline": false, "text": "New in" },
  "body":   { "html_tag": "p",  "color_hex": "#FFFFFF", "text": "Shop the drop" },
  "horizontal_alignment": "center",
  "vertical_alignment": "end",
  "background_overlay": "rgba(0,0,0,0.35)",
  "overlay_style": "gradient",
  "text_shadow": true,
  "gap": "4px"
}
```

`html_tag` is required inside `header`/`body`; alignments default to
`center`; text color defaults to white. Ignore JSON keys you do not
recognize: the default-language value can contain internal bookkeeping
(such as a `translations` map) that you should not render. Use
`@inContext(language: ...)` to get localized text instead, see
[Translations](#translations).

<Note>
  Forward compatibility: a block may in the future carry a `content` field
  (a metaobject reference). When it is set, read the media, link and text
  fields from the referenced metaobject and keep reading the placement
  fields (`index`, `span_rows`, `span_columns`, `aspect_ratio`,
  `visibility`) from the block itself. Blocks without `content` work as
  described above.
</Note>

### Media URLs

Image URLs are standard Shopify CDN URLs. Request appropriately sized
variants with query parameters, for example
`...campaign.jpg?width=1024` or, to match a container of known aspect
ratio, `...?width=1024&crop=center&height=683`. The web renderer requests
widths from 128 to 4096 depending on the rendered size; native apps should
do the equivalent for their screen densities.

For video, the web renderer prefers the HLS playlist on mobile (adaptive
bitrate) and the largest MP4 on desktop, and plays it muted, looped,
inline, only while visible.

## Image settings

Image settings override which images represent a product inside one
specific collection (they do not affect the product elsewhere).

```graphql theme={null}
query CollectionImageSettings($handle: String!) @inContext(country: SE) {
  collection(handle: $handle) {
    metafield(namespace: "app--93504864257--image-setting", key: "image_setting") {
      references(first: 250) {
        pageInfo {
          hasNextPage
          endCursor
        }
        nodes {
          ... on Metaobject {
            id
            product: field(key: "product") {
              reference { ... on Product { id } }
            }
            defaultImage: field(key: "default_image") {
              reference { ... on MediaImage { image { url width height altText } } }
            }
            hoverImage: field(key: "hover_image") {
              reference { ... on MediaImage { image { url width height altText } } }
            }
          }
        }
      }
    }
  }
}
```

Semantics: for the referenced product, render `default_image` as the
product card's primary image in this collection. If your card design shows
a hover or second image and `hover_image` is set, use it for that.
`default_image` is always set; `hover_image` is optional.

## Product duplicates

A duplicate shows a product an extra time in the grid, with its own
imagery, at a chosen position.

```graphql theme={null}
query CollectionDuplicates($handle: String!) @inContext(country: SE) {
  collection(handle: $handle) {
    metafield(namespace: "app--93504864257--product-duplicate", key: "product_duplicate") {
      references(first: 250) {
        pageInfo {
          hasNextPage
          endCursor
        }
        nodes {
          ... on Metaobject {
            id
            handle
            product: field(key: "product") {
              reference { ... on Product { id } }
            }
            defaultImage: field(key: "default_image") {
              reference { ... on MediaImage { image { url width height altText } } }
            }
            hoverImage: field(key: "hover_image") {
              reference { ... on MediaImage { image { url width height altText } } }
            }
            index: field(key: "index") { value }
            createdAt: field(key: "created_at") { value }
          }
        }
      }
    }
  }
}
```

Semantics:

* Render the referenced product a second time, using the duplicate's
  `default_image` (and `hover_image` if set) instead of the product's
  images. Everything else on the card (title, price, link) is the
  product's own.
* `index`, when set, is the target position in the combined
  products-plus-duplicates list. When `index` is null, the duplicate goes
  directly after its original product. `created_at` (a Unix timestamp in
  milliseconds) orders multiple duplicates of the same product.
* Image settings do not apply to duplicates; a duplicate always uses its
  own imagery, even when its original product has an image setting.
* The metaobject `handle` is a stable identifier for the duplicate, useful
  as a list key.

The exact insertion algorithm, including edge cases, is in the
[rendering spec](/shopify-lite/headless/grid-rendering-spec#9-duplicates).

## Markets

When a collection has market-specific merchandising, Depict publishes a
`depict_multi_market_collection` metaobject per (collection, market). Its
handle is deterministic:

```
collection_{collectionId}_market_{marketId}
```

where both IDs are the numeric parts of the Shopify GIDs (for example
collection `gid://shopify/Collection/402462310713` and market
`gid://shopify/Market/37750866085` give
`collection_402462310713_market_37750866085`).

Get the buyer's market ID from the Storefront API itself:

```graphql theme={null}
query CurrentMarket @inContext(country: DE) {
  localization {
    market { id handle }
  }
}
```

Then fetch the market's overrides by handle:

```graphql theme={null}
query MarketOverrides {
  metaobject(handle: {
    type: "depict_multi_market_collection",
    handle: "collection_402462310713_market_37750866085"
  }) {
    contentBlocks: field(key: "content_blocks") {
      references(first: 100) {
        nodes { ... on Metaobject { id } }
      }
    }
    productDuplicates: field(key: "product_duplicates") {
      references(first: 250) {
        nodes { ... on Metaobject { id } }
      }
    }
    imageSettings: field(key: "image_settings") {
      references(first: 250) {
        nodes { ... on Metaobject { id } }
      }
    }
    productOrder: field(key: "product_order") {
      reference {
        ... on Metaobject {
          products0: field(key: "products_0") {
            references(first: 128) {
              nodes { ... on Product { id handle } }
            }
          }
          products1: field(key: "products_1") {
            references(first: 128) {
              nodes { ... on Product { id handle } }
            }
          }
          # ...continue with products_2 through products_39 as needed
        }
      }
    }
  }
}
```

(Select the same inner fields on the referenced block, duplicate and image
setting metaobjects as in the per-piece queries above; they are the same
metaobject types. The metaobject also carries required
`collection_reference` and `market_id` fields, which back the handle
scheme; you do not need them for reads.)

### Fallback rules

For each of the four pieces, resolve the market's data like the web
storefront does:

1. If the `depict_multi_market_collection` metaobject for (collection,
   current market) exists **and** the piece's field is set, use it.
2. Otherwise use the default: the collection metafield for blocks,
   duplicates and image settings, and the collection's own product order
   for ordering.

Fallback is per piece: a market can override just the product order while
inheriting the default blocks, or the other way around.

### Per-market product order

The `product_order` field references a `depict_market_products` metaobject
holding the full ordered product list, split across 40 fields
`products_0` through `products_39`, each a `list.product_reference` of up
to 128 products (5120 products maximum). Concatenate the chunks in field
order, skipping empty ones; check all 40 fields rather than stopping at
the first empty chunk (chunks are written contiguously, but scanning all
40 matches the web storefront and is robust). Products that are not
published to the current country resolve to null in the references; skip
them.

## Translations

Content block text is localized through Shopify's native translations. Add
`@inContext(language: ...)` to your query and the `text` field's `value`
comes back in that locale (with the header and body text swapped to the
translation and all styling preserved):

```graphql theme={null}
query LocalizedBlocks($handle: String!)
@inContext(country: DE, language: DE) {
  collection(handle: $handle) {
    metafield(namespace: "app--93504864257--content-blocks", key: "content_block") {
      references(first: 100) {
        nodes {
          ... on Metaobject {
            id
            text: field(key: "text") { value }
          }
        }
      }
    }
  }
}
```

Only the `text` field is translated. Other fields, including `alt_text`,
are deliberately kept identical across locales.

## Publishing model and freshness

* Merchandisers work with drafts and versions inside Depict. None of that
  is part of your read path: publishing (immediate or scheduled) updates
  the collection sort order, the collection metafields and the per-market
  metaobjects to the published state, and those are the only things the
  queries on this page read.
* Collection reordering is applied by Shopify as an asynchronous job, so a
  fresh publish can take a short moment to be fully visible.
* Scheduled publishes are applied by Depict on a schedule; expect a
  scheduled version to be live within about 30 minutes of its scheduled
  time.
* Cache freely. All of this data changes only when the merchant publishes.
  Re-fetching per screen view, or on a minutes-level TTL, is plenty.

## Matching the web storefront

A few behaviors worth mirroring for a consistent experience:

* Content blocks and duplicates are positional. The web storefront hides
  both whenever the shopper applies a different sort order or filters
  (except a pure in-stock availability filter). Do the same when your app
  re-sorts or filters a collection. Image settings still apply in sorted
  and filtered views.
* Column counts come from the shop metafield
  `column_settings` (`[desktopColumns, mobileColumns]`, defaults 4 and 2).
  Using the merchant's configured counts makes block placement match what
  the merchandiser designed. Block indexes are authored against the
  desktop count; the rendering spec's remapping algorithm keeps blocks
  anchored to the same products when you render fewer columns. The shop
  metafield `grid_settings` (JSON) additionally carries the web grid's
  spacing values, for example
  `{"desktop": {"columnSpacing": "8px", "rowSpacing": "8px", "margin": ""}, "mobile": {...}}`,
  plus web-renderer options (`customCss`, `removeProductCardPadding`,
  `alignBlocksToImage`), if you want visual parity.
* During a Depict collection A/B test, the collection carries an
  `ab_test_v3` metafield (namespace `app--93504864257--ab-test`)
  referencing `depict_ab_test_v3` metaobjects that hold the test window.
  While a test is running, web sessions randomly assigned to the control
  group hide content blocks, duplicates and image settings. A native app
  that ignores this always shows the treatment, which skews the
  merchant's test results; if a test is active, consider splitting
  sessions the same way or agreeing with the merchant on how app traffic
  should count.

For the full, testable layout algorithm, continue to the
[collection grid rendering specification](/shopify-lite/headless/grid-rendering-spec).
