Structured Data Guide

Shopify Subscription Pricing Structured Data

AI shopping agents compare prices using your JSON-LD Offer blocks — not your storefront UI. If your subscribe-and-save discount only appears in the UI (via Recharge or Stay AI), agents see your more expensive one-time price and rank you below competitors who expose their subscription rate in structured data.

TL;DR Add a second Offer block to your product JSON-LD with the subscription price, a UnitPriceSpecification billing period, and a description of the recurring commitment. This surfaces the true lowest price to AI agents and signals that your product is available on a recurring basis — a positive quality signal for replenishment-category queries.

The Subscription Price Gap Problem

Most Shopify stores with subscription products (via Recharge, Stay AI, Seal Subscriptions, or native Shopify Subscriptions) have a pricing display gap in their structured data. The pattern looks like this:

Displayed to user (UI) JSON-LD Offer price AI agent sees
Subscribe: $22.99/mo (save 15%) $26.99 (one-time price only) $26.99 — may rank lower in price comparisons
One-time: $26.99 $26.99 Only sees one-time price; subscription discount invisible

ChatGPT Shopping and Perplexity Shopping both aggregate prices from structured data and compare them across stores. A store selling protein powder at "$26.99" in JSON-LD appears more expensive than a competitor with a "$23.99" one-time price, even when your subscribe-and-save rate is $22.99 — the cheapest option in the category.

schema.org Properties for Subscription Pricing

UnitPriceSpecification with billingDuration

The most accurate schema.org encoding for a recurring subscription price uses priceSpecification on the Offer, typed as UnitPriceSpecification:

{
  "@type": "Offer",
  "name": "Subscribe and save — monthly",
  "price": "22.99",
  "priceCurrency": "USD",
  "availability": "https://schema.org/InStock",
  "priceSpecification": {
    "@type": "UnitPriceSpecification",
    "price": "22.99",
    "priceCurrency": "USD",
    "billingDuration": "P1M",
    "billingStart": 0,
    "description": "Recurring monthly subscription. Cancel anytime."
  },
  "description": "Monthly subscription — 15% off vs. one-time price."
}

ISO 8601 duration values for common billing periods:

Billing period ISO 8601 duration billingDuration value
WeeklyP7D"P7D"
MonthlyP1M"P1M"
Every 6 weeksP6W"P6W"
QuarterlyP3M"P3M"
Every 6 monthsP6M"P6M"
AnnualP1Y"P1Y"

Dual Offer pattern: one-time + subscription

The full pattern adds both offer types to the offers array. This gives AI agents both price points and lets them select the appropriate one for the query context ("cheapest protein powder" vs. "protein powder subscription"):

{%- assign one_time_price = product.price | money_without_currency -%}
{%- comment -%}
  Store subscription price in a metafield: product.metafields.subscriptions.monthly_price
  e.g., "22.99" — set by your subscription app or manually
{%- endcomment -%}
{%- assign sub_price = product.metafields.subscriptions.monthly_price -%}

"offers": [
  {
    "@type": "Offer",
    "name": "One-time purchase",
    "price": {{ one_time_price | json }},
    "priceCurrency": {{ cart.currency.iso_code | json }},
    "availability": "https://schema.org/InStock",
    "priceValidUntil": "{{ 'now' | date: '%s' | plus: 2592000 | date: '%Y-%m-%d' }}"
  }
  {%- if sub_price != blank -%}
  ,{
    "@type": "Offer",
    "name": "Subscribe and save",
    "price": {{ sub_price | json }},
    "priceCurrency": {{ cart.currency.iso_code | json }},
    "availability": "https://schema.org/InStock",
    "priceSpecification": {
      "@type": "UnitPriceSpecification",
      "price": {{ sub_price | json }},
      "priceCurrency": {{ cart.currency.iso_code | json }},
      "billingDuration": "P1M"
    },
    "description": "Monthly subscription. Cancel anytime."
  }
  {%- endif -%}
]

Trial Periods and Delayed Billing

For subscriptions with a free trial or a discounted first period, use billingStart to indicate when full billing begins:

{
  "@type": "Offer",
  "name": "First month free, then subscribe",
  "price": "0.00",
  "priceCurrency": "USD",
  "availability": "https://schema.org/InStock",
  "priceSpecification": {
    "@type": "UnitPriceSpecification",
    "price": "0.00",
    "priceCurrency": "USD",
    "billingDuration": "P1M",
    "billingStart": 1,
    "description": "First month free, then $22.99/month."
  }
}

The billingStart: 1 value tells agents that billing begins after period 1 (i.e., the first month is free). Some AI shopping agents will surface "first month free" framing in recommendations when this is present.

Recharge and Stay AI Integration Notes

Platform Auto JSON-LD injection Manual fix needed?
Recharge (Shopify checkout) No — modifies UI price display only Yes — add subscription Offer block to theme's product JSON-LD snippet
Stay AI No — UI-only subscription price injection Yes — read selling_plan_allocations in Liquid, emit priceSpecification block
Native Shopify Subscriptions Partial — Shopify adds selling_plan_allocation to product JSON but not full UnitPriceSpecification Partial — extend with billingDuration from selling_plan.billing_policy
Seal Subscriptions No — theme-level JS injection, not JSON-LD Yes — manual theme edit required
Bold Subscriptions No Yes

Reading native Shopify selling plans in Liquid

{%- for allocation in product.selling_plan_allocations -%}
  {%- if allocation.selling_plan.billing_policy.recurring -%}
    {%- assign sub_price = allocation.per_delivery_price | money_without_currency -%}
    {%- assign sub_interval = allocation.selling_plan.billing_policy.interval -%}
    {%- comment -%} interval values: day, week, month, year {%- endcomment -%}
    {%- break -%}
  {%- endif -%}
{%- endfor -%}

Map allocation.selling_plan.billing_policy.interval to ISO 8601: monthP1M, weekP7D, yearP1Y. Multiply by interval_count for every-N periods.

Subscription Structured Data Checklist

# Check Priority
1 Subscription price appears in JSON-LD Offer block (not only in UI) Critical
2 Subscription Offer has UnitPriceSpecification with billingDuration Critical
3 One-time and subscription prices both present in offers array High
4 Subscription Offer has descriptive name field distinguishing it from one-time High
5 Free trial encoded with billingStart = trial period count High
6 billingDuration uses ISO 8601 duration string (P1M, P3M, P1Y) Medium
7 Subscription Offer has description noting "cancel anytime" or commitment terms Medium
8 Validated via Google Rich Results Test — no subscription-related errors Medium

Related Resources

Frequently Asked Questions

How do AI shopping agents handle subscription pricing in Shopify JSON-LD?

AI shopping agents read the price from the Offer block's price field. For subscription products, this is a problem: Shopify defaults to displaying the one-time price in JSON-LD even when a cheaper subscription price exists. Agents compare that JSON-LD price against competitor prices — if your subscription discount isn't in structured data, you look more expensive than you are. Add a second Offer block with the subscription price and billingDuration.

What schema.org properties should I use for subscription billing period?

Use the Offer's priceSpecification field with a UnitPriceSpecification type, and set the billingDuration property to an ISO 8601 Duration value (e.g., P1M for monthly, P3M for quarterly, P1Y for annual). This is the most semantically accurate encoding for recurring billing.

Does Recharge or Stay AI automatically add subscription JSON-LD?

As of 2026, neither Recharge nor Stay AI automatically injects subscription-specific JSON-LD. Both modify the storefront UI to show subscription pricing, but the JSON-LD typically still reflects the base Shopify product JSON with one-time prices. You need to manually extend your theme's JSON-LD snippet to detect subscription selling plans.

How do I differentiate one-time price from subscribe-and-save price in JSON-LD?

Add two separate Offer blocks to the Product's offers array: one for the one-time purchase price and one for the subscription price. Give each a distinct name field and set the subscription Offer's priceSpecification to a UnitPriceSpecification with billingDuration. This dual-offer pattern is recognized by Google's rich results parser and ChatGPT Shopping's offer comparison logic.

Find Subscription Pricing Gaps in Your Catalog

CatalogScan detects products where subscription prices exist in the UI but are missing from JSON-LD Offer blocks — including Recharge, Stay AI, and native Shopify Subscriptions setups.

Scan your store free