Skip to main content
This feature is available on the Pro plan only. The public metafields are written and kept in sync automatically whenever you save a bundle in the app.

Overview

On the Pro plan, Panori Bundles writes the bundle configuration into a public metafield namespace (panda_bundles) on every mother variant. This lets you build a fully custom bundle picker directly in your theme’s Liquid — no Theme App Extension block required.

Available Metafields

Each mother variant gets a metafield with the full bundle configuration for that variant.
ScopeNamespaceKeyTypeLiquid accessor
Productpanda_bundlesconfigjsonproduct.metafields.panda_bundles.config
Mother Variantpanda_bundlesbundle_configjsonvariant.metafields.panda_bundles.bundle_config
Mother Variantpanda_bundleschild_variantslist.variant_referencevariant.metafields.panda_bundles.child_variants
child_variants is a list.variant_reference — Shopify resolves each entry to a full variant object in Liquid, giving you live access to .price, .title, .image, .available, and all other variant properties.

Data structure

{
  "bundleId": "a1b2c3d4-...",
  "steps": [
    {
      "id": "step-uuid",
      "label": "Choose your base",
      "maxVariants": 1,
      "isGift": false,
      "childVariants": [
        {
          "variantId": "gid://shopify/ProductVariant/111",
          "productId": "gid://shopify/Product/222",
          "price": "29.99",
          "available": true,
          "imageUrl": "https://cdn.shopify.com/..."
        }
      ]
    }
  ],
  "discount": {
    "type": "percentage",
    "value": 10
  }
}
discount.type can be "percentage", "fixed", or "custom". For "custom", the price lives directly on the Shopify variant — no calculation needed.

Building the UI in Liquid

Use bundle_config for step structure and child_variants for live variant data. Combine them by matching variantId from the config against the resolved variant objects.
{% for variant in product.variants %}
  {% assign bundle_configs = variant.metafields.panda_bundles.bundle_config.value %}
  {% assign child_variants = variant.metafields.panda_bundles.child_variants.value %}

  {% if bundle_configs != blank %}
    {% assign bundle = bundle_configs[0] %}

    <div data-bundle-variant="{{ variant.id }}">

      {% for step in bundle.steps %}
        <div data-step-id="{{ step.id }}">
          <p><strong>{{ step.label }}</strong></p>

          {% for cv in step.childVariants %}
            {%- comment -%}
              Look up the live variant object from child_variants by matching the GID.
              This gives us live price, title, image and availableForSale.
            {%- endcomment -%}
            {% assign live_variant = nil %}
            {% for v in child_variants %}
              {% if v.id == cv.variantId %}
                {% assign live_variant = v %}
                {% break %}
              {% endif %}
            {% endfor %}

            {% if live_variant.available %}
              <label>
                <input
                  type="checkbox"
                  data-step-id="{{ step.id }}"
                  data-variant-id="{{ cv.variantId }}"
                  data-max="{{ step.maxVariants | default: 99 }}"
                />
                {% if live_variant.image %}
                  {{ live_variant.image | image_url: width: 96 | image_tag: loading: 'lazy', width: 48, height: 48 }}
                {% endif %}
                <span>{{ live_variant.title }}</span>
                <span>{{ live_variant.price | money }}</span>
              </label>
            {% endif %}

          {% endfor %}
        </div>
      {% endfor %}

    </div>
  {% endif %}
{% endfor %}
child_variants is a list.variant_reference — Shopify resolves it to actual variant objects at render time. Prices, availability, and images are always live. You do not need to manage staleness.

Submitting the Bundle to Cart

When the customer confirms their selection, you must POST to /cart/add.js with the selected variants plus a _bundle_config_json property on the first line item and a shared _bundle_id across all items.
1

Collect selections

Gather the selected variantId per step from your UI state.
2

Build the payload

Construct the _bundle_config_json from the bundle metafield and the customer’s selections.
const bundleInstanceId = crypto.randomUUID();

// bundle = variant.metafields.panda_bundles.bundle_config (from a data attribute or API)
// getSelections(stepId) returns [{ variantId: "gid://...", quantity: 1 }]
const payload = JSON.stringify({
  bundleId: bundle.bundleId,
  motherVariantId: "gid://shopify/ProductVariant/MOTHER_ID",
  steps: bundle.steps.map((step) => ({
    stepId: step.id,
    selectedVariants: getSelections(step.id),
  })),
});
3

POST to /cart/add.js

Add each selected child variant as a separate line item. Attach _bundle_config_json to the first item only, and _bundle_id to all items.
// getSelections returns [{ variantId, quantity }] — quantity can be > 1 for the same variant
const allSelections = bundle.steps.flatMap((step) => getSelections(step.id));

const items = allSelections.map(({ variantId, quantity }, idx) => ({
  id: numericId(variantId), // strip the GID prefix
  quantity,
  properties: {
    _bundle_id: bundleInstanceId,
    ...(idx === 0 ? { _bundle_config_json: payload } : {}),
  },
}));

await fetch("/cart/add.js", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ items }),
});

function numericId(gid) {
  return gid.split("/").at(-1);
}

Server-side Validation

Do not include discount, isGift, maxVariants, or allowedVariantIds in the _bundle_config_json payload. These fields are ignored — Panori Bundles reads them exclusively from the server-side metafield.
When the cart is processed, the Panori Bundles cart transform function validates every bundle instance against the saved configuration:
CheckWhat is validated
Bundle existsbundleId must match a known bundle in the shop config
Mother variantmotherVariantId must belong to that bundle
Step existsEvery stepId must exist on the server-side mother variant
Allowed variantsEach selectedVariantId must be in that step’s childVariants list
Max selectionsSum of all quantity values in a step must not exceed maxVariants
QuantityEach cart line’s quantity must exactly match the declared quantity in selectedVariants
If any check fails, the bundle is not merged and the items remain as separate, full-price lines in the cart.