Optimization Guide

Shopify UnitPriceSpecification Schema — Per-Unit Pricing for Grocery, Bulk & Fabric Stores

AI shopping agents answering "cheapest olive oil per litre" or "best value protein powder per 100g" need normalized per-unit prices to compare across differently-sized packages — and Shopify's default JSON-LD outputs only the total transaction price, making cross-product comparison impossible. UnitPriceSpecification is the schema.org type that encodes this normalized price: price per 100g, per litre, per metre, per piece. It's legally required for EU stores selling pre-packaged goods and is increasingly used by AI shopping comparison agents globally.

TL;DR Add a priceSpecification block of @type: "UnitPriceSpecification" on your Offer object. Set price to the per-unit amount, priceCurrency to your currency code, referenceQuantity to the base unit (e.g., 100 GRM for per-100g, 1 LTR for per-litre), unitCode to the UN/CEFACT code, and unitText to the human-readable label. For fabric and bulk stores with fractional quantities, add billingIncrement for the minimum purchasable step.

Why Shopify Stores Miss Unit Pricing in Structured Data

Shopify has native unit pricing built into the Liquid theme layer: variant.unit_price returns the price per unit (e.g., $2.99), and variant.unit_price_measurement returns the measurement object (e.g., 100g). But this information lives entirely in the HTML price display layer and is never output in the product JSON-LD that structured data crawlers consume.

The consequence for AI shopping agents is significant: a query like "cheapest ground coffee per 100g under $3" requires comparing unit prices across a 250g bag at $6.49, a 500g bag at $11.99, and a 1kg bag at $21.99. Without UnitPriceSpecification in JSON-LD, an AI agent must either parse the product description for weight information and calculate the unit price itself (error-prone), or exclude your products from comparison entirely.

Store types that need UnitPriceSpecification

Store type Reference unit Example query AI agents must answer
Grocery / food per 100g or per kg "cheapest organic oats per 100g," "best value almond butter per kg"
Coffee / tea per 100g "single-origin coffee under $4 per 100g," "loose leaf tea price per 100g"
Liquids / beverages per litre or per 100ml "cheapest cold-pressed olive oil per litre," "kombucha price per 100ml"
Supplements / protein per 100g or per serving "best value whey protein per 100g," "creatine price per gram"
Fabric / textiles per metre or per yard "linen fabric under $15 per metre," "denim fabric price per yard"
Bulk hardware per piece (C62) "M8 bolts price per 100 pieces," "cable ties cost per unit"
Cleaning supplies per litre or per 100ml "dish soap concentrated price per litre," "laundry detergent cost per wash"

UnitPriceSpecification vs. Standard Offer Price

The standard Offer price is the total amount a buyer pays at checkout — it drives the transaction. UnitPriceSpecification is an additional price specification that normalizes the price to a standard reference unit for comparison shopping. Both coexist on the same product: the standard price for the transaction, the unit price for comparison.

Schema.org allows two ways to attach UnitPriceSpecification to a product:

  1. priceSpecification on the Offer — attach a UnitPriceSpecification object alongside the standard price field. This is the cleaner pattern when the product has a single selling unit.
  2. offers array with a dedicated Offer of type UnitPriceSpecification — output a second Offer object where @type is UnitPriceSpecification. This pattern is required when the unit price is the primary price (e.g., fabric sold strictly by the metre).
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Single-Origin Ethiopian Coffee Beans — Medium Roast — 500g",
  "description": "Washed Yirgacheffe coffee beans, medium roast. Floral and bright with notes of jasmine, bergamot, and stone fruit. Third-wave roasted weekly.",
  "sku": "COFFEE-ETH-500G",
  "brand": { "@type": "Brand", "name": "Bright Grounds Roastery" },
  "offers": {
    "@type": "Offer",
    "price": "16.99",
    "priceCurrency": "USD",
    "availability": "https://schema.org/InStock",
    "url": "https://example.com/products/ethiopian-coffee-500g",
    "priceSpecification": [
      {
        "@type": "UnitPriceSpecification",
        "price": "3.40",
        "priceCurrency": "USD",
        "referenceQuantity": {
          "@type": "QuantitativeValue",
          "value": "100",
          "unitCode": "GRM",
          "unitText": "100g"
        },
        "unitCode": "GRM",
        "unitText": "per 100g"
      }
    ]
  }
}
</script>

In this example: the Offer price $16.99 is the checkout price for the 500g bag. The UnitPriceSpecification price $3.40 is the normalized price per 100g, computed from 16.99 / 5 = 3.398 ≈ $3.40. AI shopping agents use the $3.40 figure to compare this product against a 250g bag at $9.49 (= $3.80/100g) and a 1kg bag at $29.99 (= $3.00/100g).

UN/CEFACT Unit Code Reference

Unit Code Store type Example referenceQuantity
Gram GRM Coffee, spices, supplements value: "100", unitCode: "GRM" (per 100g)
Kilogram KGM Bulk food, flour, nuts value: "1", unitCode: "KGM" (per kg)
Milligram MGM Pharmaceuticals, supplements by dose value: "1000", unitCode: "MGM" (per 1000mg = 1g)
Litre LTR Olive oil, beverages, cleaning products value: "1", unitCode: "LTR" (per litre)
Millilitre MLT Perfume, skincare, small-batch liquids value: "100", unitCode: "MLT" (per 100ml)
Metre MTR Fabric, ribbon, rope, tubing value: "1", unitCode: "MTR" (per metre)
Yard YRD Fabric (US/imperial), quilting value: "1", unitCode: "YRD" (per yard)
Each (unit) C62 Hardware, fasteners, tiles sold in packs value: "1", unitCode: "C62" (per piece)
Roll ROL Wallpaper, tape, wrapping paper value: "1", unitCode: "ROL" (per roll)

Important: UN/CEFACT unit codes are case-sensitive and must be uppercase — KGM not kgm, LTR not ltr. Schema.org validators will accept lowercase, but Google's structured data processing and AI agent parsers trained on the official vocabulary expect the canonical uppercase form. Always pair unitCode with unitText for human-readable display fallback.

Fabric Store Pattern: Metre-Based Unit Pricing

Fabric sold by the metre is a specialized case where the per-unit (per-metre) price IS the primary pricing unit — buyers select how many metres they want, and the total price is calculated at checkout. This requires a different schema pattern: the Offer itself uses @type: "UnitPriceSpecification" as the primary price type.

{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Belgian Linen Fabric — Natural Undyed — 150cm Width",
  "description": "100% Belgian linen, natural undyed, 150cm wide. Prewashed and preshrunk. Suitable for clothing, home décor, and upholstery.",
  "sku": "FABRIC-LINEN-BE-NATURAL-150",
  "brand": { "@type": "Brand", "name": "Atelier Textile" },
  "offers": {
    "@type": "UnitPriceSpecification",
    "price": "18.50",
    "priceCurrency": "USD",
    "availability": "https://schema.org/InStock",
    "url": "https://example.com/products/belgian-linen-natural",
    "referenceQuantity": {
      "@type": "QuantitativeValue",
      "value": "1",
      "unitCode": "MTR",
      "unitText": "metre"
    },
    "unitCode": "MTR",
    "unitText": "per metre",
    "billingIncrement": 0.1,
    "description": "Minimum order 0.5m. Sold in 0.1m increments. Enter number of metres required in quantity field."
  }
}

The billingIncrement: 0.1 signals that buyers can purchase in 0.1-metre steps (10cm increments). The minimum purchase is 0.5 metres, which would be expressed separately via a QuantitativeValue on an eligibleQuantity property if needed for procurement queries. For most fabric stores, the billingIncrement and the description field covering minimum order requirements provide sufficient context for AI shopping agents.

Multi-Size Product: Unit Price Across Variants

When a single product is available in multiple sizes (250g, 500g, 1kg), each variant has a different unit price. In Shopify structured data, each size variant should be a separate Offer with its own UnitPriceSpecification. Wrap all size variants in a ProductGroup to signal that they are the same product in different sizes.

{
  "@context": "https://schema.org",
  "@type": "ProductGroup",
  "name": "Grass-Fed Whey Protein Isolate — Natural Flavour",
  "description": "Grass-fed whey protein isolate, 90% protein content, unflavoured. Cold-processed, no artificial sweeteners.",
  "brand": { "@type": "Brand", "name": "Clean Protein Co." },
  "variesBy": "https://schema.org/size",
  "hasVariant": [
    {
      "@type": "Product",
      "name": "Grass-Fed Whey Protein Isolate — 500g",
      "sku": "WPI-NATURAL-500G",
      "size": "500g",
      "offers": {
        "@type": "Offer",
        "price": "34.99",
        "priceCurrency": "USD",
        "availability": "https://schema.org/InStock",
        "priceSpecification": [{
          "@type": "UnitPriceSpecification",
          "price": "7.00",
          "priceCurrency": "USD",
          "referenceQuantity": { "@type": "QuantitativeValue", "value": "100", "unitCode": "GRM", "unitText": "100g" },
          "unitText": "per 100g"
        }]
      }
    },
    {
      "@type": "Product",
      "name": "Grass-Fed Whey Protein Isolate — 1kg",
      "sku": "WPI-NATURAL-1KG",
      "size": "1kg",
      "offers": {
        "@type": "Offer",
        "price": "59.99",
        "priceCurrency": "USD",
        "availability": "https://schema.org/InStock",
        "priceSpecification": [{
          "@type": "UnitPriceSpecification",
          "price": "6.00",
          "priceCurrency": "USD",
          "referenceQuantity": { "@type": "QuantitativeValue", "value": "100", "unitCode": "GRM", "unitText": "100g" },
          "unitText": "per 100g"
        }]
      }
    },
    {
      "@type": "Product",
      "name": "Grass-Fed Whey Protein Isolate — 2kg",
      "sku": "WPI-NATURAL-2KG",
      "size": "2kg",
      "offers": {
        "@type": "Offer",
        "price": "104.99",
        "priceCurrency": "USD",
        "availability": "https://schema.org/InStock",
        "priceSpecification": [{
          "@type": "UnitPriceSpecification",
          "price": "5.25",
          "priceCurrency": "USD",
          "referenceQuantity": { "@type": "QuantitativeValue", "value": "100", "unitCode": "GRM", "unitText": "100g" },
          "unitText": "per 100g"
        }]
      }
    }
  ]
}

With this markup, an AI agent responding to "best value protein powder per 100g" can compute: 500g = $7.00/100g, 1kg = $6.00/100g, 2kg = $5.25/100g — and correctly identify the 2kg option as the best unit value. Without the UnitPriceSpecification, the agent sees $34.99, $59.99, and $104.99 — total prices that cannot be compared without knowing each product's weight.

Dawn Liquid Snippet: UnitPriceSpecification from Shopify Unit Pricing

Shopify exposes unit pricing via variant.unit_price (the per-unit price in cents / smallest currency unit) and variant.unit_price_measurement (an object with measured_type, quantity_value, quantity_unit, reference_value, reference_unit). Map these to UnitPriceSpecification properties.

{% comment %} unit-price-schema.liquid — UnitPriceSpecification from Shopify native unit pricing {% endcomment %}
{% assign v = product.selected_or_first_available_variant %}

{% if v.unit_price_measurement %}
{% assign m = v.unit_price_measurement %}

{% comment %} Map Shopify measurement units to UN/CEFACT codes {% endcomment %}
{% assign unit_code = 'C62' %}
{% assign unit_display = m.reference_unit %}
{% case m.reference_unit %}
  {% when 'g' %}{% assign unit_code = 'GRM' %}{% assign unit_display = 'g' %}
  {% when 'kg' %}{% assign unit_code = 'KGM' %}{% assign unit_display = 'kg' %}
  {% when 'mg' %}{% assign unit_code = 'MGM' %}{% assign unit_display = 'mg' %}
  {% when 'ml' %}{% assign unit_code = 'MLT' %}{% assign unit_display = 'ml' %}
  {% when 'l' %}{% assign unit_code = 'LTR' %}{% assign unit_display = 'l' %}
  {% when 'm' %}{% assign unit_code = 'MTR' %}{% assign unit_display = 'm' %}
  {% when 'cm' %}{% assign unit_code = 'CMT' %}{% assign unit_display = 'cm' %}
  {% when 'fl_oz' %}{% assign unit_code = 'OZA' %}{% assign unit_display = 'fl oz' %}
  {% when 'oz' %}{% assign unit_code = 'ONZ' %}{% assign unit_display = 'oz' %}
  {% when 'lb' %}{% assign unit_code = 'LBR' %}{% assign unit_display = 'lb' %}
  {% when 'ft' %}{% assign unit_code = 'FOT' %}{% assign unit_display = 'ft' %}
{% endcase %}

{% assign unit_price_decimal = v.unit_price | money_without_currency %}

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": {{ product.title | json }},
  "offers": {
    "@type": "Offer",
    "price": {{ v.price | money_without_currency | json }},
    "priceCurrency": {{ cart.currency.iso_code | json }},
    "availability": {% if product.available %}"https://schema.org/InStock"{% else %}"https://schema.org/OutOfStock"{% endif %},
    "priceSpecification": [
      {
        "@type": "UnitPriceSpecification",
        "price": {{ unit_price_decimal | json }},
        "priceCurrency": {{ cart.currency.iso_code | json }},
        "referenceQuantity": {
          "@type": "QuantitativeValue",
          "value": {{ m.reference_value | json }},
          "unitCode": {{ unit_code | json }},
          "unitText": {{ unit_display | json }}
        },
        "unitCode": {{ unit_code | json }},
        "unitText": {{ "per " | append: m.reference_value | append: unit_display | json }}
      }
    ]
  }
}
</script>
{% endif %}
		

Save as snippets/unit-price-schema.liquid. Render from your product template alongside your existing product JSON-LD. The snippet fires only when the variant has unit pricing enabled in Shopify admin — it self-suppresses for products where unit pricing is not applicable.

5 Common UnitPriceSpecification Mistakes

# Mistake Impact Fix
1 Using lowercase UN/CEFACT unit codes (kgm, ltr) Google's structured data processing and AI agent parsers expect the canonical uppercase form. Lowercase codes may validate but fail in downstream systems that apply strict UN/CEFACT lookup tables Always use uppercase: KGM, LTR, GRM, MTR, MLT, C62. Add unitText for human-readable display alongside the code
2 Computing unit price from the wrong package weight If a 500g bag of coffee is priced at $16.99, the per-100g price is $3.398 (~$3.40). Using the raw display weight "500" without the division by 5 yields $33.98 — 10x too high. AI agents performing price comparisons will surface wildly wrong values Divide Offer price by (package_quantity / reference_quantity). For per-100g: price / (weight_in_grams / 100). Round to 2 decimal places
3 Omitting referenceQuantity Without referenceQuantity, the unit price is ambiguous — "$3.40 per what?" AI agents cannot use a unit price without knowing the reference quantity. The UnitPriceSpecification is semantically incomplete Always include referenceQuantity with value, unitCode, and unitText
4 Not updating unit prices when variant prices change Hardcoded unit prices become stale during promotions, price changes, or seasonal adjustments Use the Liquid computation pattern — variant.unit_price | money_without_currency — to derive unit price dynamically from Shopify's native unit pricing
5 Using UnitPriceSpecification only on some variants of a multi-size product AI comparison shopping requires unit prices on all variants. A missing unit price on one size forces the AI to exclude it from the comparison Enable Shopify's unit pricing on all size variants. The Liquid snippet above fires automatically for any variant with unit_price_measurement set

Frequently Asked Questions

What is the difference between Offer price and UnitPriceSpecification?

The Offer price is the total checkout price (e.g., $16.99 for a 500g bag). UnitPriceSpecification is the normalized per-reference-unit price (e.g., $3.40 per 100g) for comparison shopping. Both coexist: the Offer price drives the transaction; the unit price enables cross-product comparison. Shopify's Liquid exposes both via variant.price and variant.unit_price, but only the total price appears in default product JSON-LD.

What UN/CEFACT unit codes should I use for common store types?

Common codes: GRM (gram), KGM (kilogram), LTR (litre), MLT (millilitre), MTR (metre), YRD (yard), C62 (each/piece). All must be uppercase. Always pair with unitText for human-readable display. See Shopify Metafield Types Reference for storing reference quantities in metafields.

Is UnitPriceSpecification required by EU law for Shopify stores?

Yes for EU/EEA stores — the EU Price Indication Directive (98/6/EC, amended by 2019/2161) mandates unit price display for pre-packaged goods sold to consumers. Shopify's built-in unit pricing covers the HTML display layer but does not output the unit price in JSON-LD. For EU store setup, see Shopify International SEO & hreflang.

How do I use referenceQuantity in UnitPriceSpecification?

referenceQuantity defines what the unit price is per: { '@type': 'QuantitativeValue', 'value': '100', 'unitCode': 'GRM' } = per 100g. The Shopify Liquid properties variant.unit_price_measurement.reference_value and .reference_unit provide these natively when unit pricing is enabled in Shopify admin. Map .reference_unit to the UN/CEFACT code using the case statement in the Liquid snippet above.

How does billingIncrement differ from referenceQuantity?

referenceQuantity is the comparison basis (per 100g, per metre). billingIncrement is the minimum purchasable step — relevant for fabric and bulk stores selling in fractional increments (0.1m, 50ml). For volume pricing by minimum order quantity in B2B contexts, see Shopify Wholesale & B2B Pricing Schema.

Are your per-unit prices invisible to AI comparison shopping?

CatalogScan audits your Shopify store's structured data and flags missing UnitPriceSpecification, incorrect unit codes, and stale unit prices across your entire product catalog — keeping your grocery, fabric, or bulk store competitive in AI-powered price comparison.

Run Free Scan

Related Resources