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.
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 |
|---|---|---|
| Weekly | P7D | "P7D" |
| Monthly | P1M | "P1M" |
| Every 6 weeks | P6W | "P6W" |
| Quarterly | P3M | "P3M" |
| Every 6 months | P6M | "P6M" |
| Annual | P1Y | "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: month → P1M, week → P7D, year → P1Y. 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
- Shopify Subscription Products for AI Agents — broader guide to subscription product catalog signals beyond pricing structured data.
- Shopify Subscription Box Structured Data — JSON-LD patterns for curated subscription box products with rotating contents.
- Shopify Schema Markup Guide — full Offer block reference including priceSpecification types.
- AI Shopping Agent Product Ranking Factors — how pricing accuracy in structured data affects recommendation frequency.
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