Table of contents
- Quick summary
- Why a shopping agent can't just read your price off the page
- What the schema actually needs to say
- Path 1: any platform, hand-authored JSON-LD
- Path 2: platform-native, no plugin needed
- Two platforms worth knowing the limits of
- How to verify it worked
- Where AIScan fits, and where it doesn't
- Fix this, then check the rest
Quick summary
| If you want to… | Do this | Where | AIScan check |
|---|---|---|---|
| Let an agent answer "what does this cost" without reading prose | Add Offer or AggregateOffer JSON-LD to the pricing page | A <script type="application/ld+json"> block | M4 |
| Cover a single fixed price | Offer with price, priceCurrency, availability | Any platform | M4 |
| Cover a range of plans or variants | AggregateOffer with lowPrice/highPrice, or one Offer per plan | Any platform | M4 |
| Bill on a recurring cycle | priceSpecification → UnitPriceSpecification with billingDuration | Any platform | M4 |
| Skip hand-authoring entirely | Nothing. WooCommerce emits it in core | WordPress + WooCommerce | M4 |
| Confirm it worked | npx aiscan-cli yoursite.com/pricing, check M4 | Terminal or aiscan.site | M4 |
An agent that has to read "starting at $12/month, billed annually" out of a paragraph is guessing at three separate facts: the number, the currency, and the billing period. Structured pricing turns that guess into a field lookup.
Why a shopping agent can't just read your price off the page
Think of a pricing page the way a human reads it: a headline number, a strikethrough for the discount, a footnote about annual billing, maybe an asterisk pointing at a currency note lower down. A person's eyes assemble that into one fact in about a second. A language model reading the same HTML has to do the same assembly from prose, and prose is where prices go wrong: a "starting at" that isn't the actual price, a currency symbol that's ambiguous between USD and CAD, a "monthly" that's actually billed yearly. None of that is a rendering failure. The page looks correct. It's a structured-data failure, because there's no field anywhere on the page that says, unambiguously, price: 12.00, priceCurrency: USD, billingDuration: 1 month.
Schema.org's Offer and AggregateOffer types exist to close exactly that gap. They put the price, the currency, the availability and the billing period into a machine-parseable block sitting next to the human-readable page, so an agent can read the field instead of parsing the sentence. AIScan's own check for this is M4, under the commerce dimension, and its own fix guide states the reasoning in one line: a JSON-LD Offer block on a pricing page lets an agent answer "what does this cost" without reading prose. Fetched from AIScan's get_check_guidance tool on 22 September 2026, so this is the platform's own current wording, not a paraphrase.
What the schema actually needs to say
Two schema.org types cover almost every pricing page. Reach for Offer when you're pricing one specific thing at one specific price. Reach for AggregateOffer when a single product or plan comes in a range, several tiers, several currencies, several variants, and you want one block that spans the range rather than one block per price.
| Field | What it carries | Example value |
|---|---|---|
price | The number a human sees | "12.00" |
priceCurrency | ISO 4217 currency code | "USD" |
priceSpecification | Nested detail for recurring billing | see below |
billingDuration | Length of one billing cycle | 1 with unitCode: "MON", or the ISO 8601 form "P1M" |
availability | Whether it can currently be bought | "https://schema.org/InStock" |
AIScan's own fix guide for M4 gives this as the base example, fetched from get_check_guidance on 22 September 2026:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Pro",
"offers": {
"@type": "Offer",
"price": "12.00",
"priceCurrency": "USD",
"priceSpecification": {
"@type": "UnitPriceSpecification",
"price": "12.00",
"priceCurrency": "USD",
"billingDuration": 1,
"billingIncrement": 1,
"unitCode": "MON"
},
"availability": "https://schema.org/InStock",
"url": "https://example.com/pricing"
}
}
</script>
That's a number-plus-unit-code form of billingDuration: 1 unit, where the unit is MON (UN/CEFACT code for month). Schema.org also accepts an ISO 8601 duration string for the same property, and it's worth knowing both forms exist because real sites use each. Squarespace's own pricing page, re-verified live on 22 September 2026 at squarespace.com/pricing, ships an OfferCatalog JSON-LD node where every plan carries monthly and annual Offer objects using priceSpecification.billingDuration written as "P1M" and "P1Y", the ISO form, alongside an eligibleRegion field the AIScan example doesn't include. Both forms validate. Pick one and stay consistent across every Offer block on your own site, because a parser that assumes one form for a whole page will misread the other.
One rule matters more than either format choice: the structured price has to match the price a human sees on the same page. AIScan's own guidance is explicit about this, and it's the one line worth keeping in view while you write the block, because an agent quoting a stale or wrong structured price to a shopper is worse than shipping no schema at all.
Path 1: any platform, hand-authored JSON-LD
This works on anything that lets you drop a <script> tag into a page: a WordPress Custom HTML block, a static site's template, a Framer Custom Code panel, a Webflow embed, or a raw HTML file.
- Decide
OfferorAggregateOffer. One fixed price for one thing getsOffer. Several plans or variants get eitherAggregateOfferwithlowPrice/highPrice, or oneOfferobject per plan nested under an array, whichever reads more naturally for your catalogue. - Fill in
priceandpriceCurrencyusing the exact number and ISO 4217 code shown on the page. Copy them, don't retype them, so a page edit can't quietly leave the schema stale. - Add
priceSpecificationfor recurring billing. Nest aUnitPriceSpecificationwithbillingDuration,billingIncrementandunitCode(or the ISO duration string), matching the AIScan example above. - Set
availability.https://schema.org/InStockfor anything currently purchasable,https://schema.org/PreOrderfor something not yet on sale,https://schema.org/Discontinuedfor a retired plan. - Wrap it in
<script type="application/ld+json">and paste that whole block into a Custom HTML block, a template partial, or a<head>include, wherever your platform lets you inject raw markup on the pricing page. - Validate before you publish. Paste the block into any JSON-LD linter, or run the verification steps below, before the page goes live. A malformed block fails silently: the page still renders, the schema just isn't read.
Path 2: platform-native, no plugin needed
WooCommerce is the clean case, because it does this automatically. WooCommerce core's own structured-data class, class-wc-structured-data.php, confirmed live on raw.githubusercontent.com/woocommerce/woocommerce on 22 September 2026, builds a Product schema block with a nested Offer carrying priceCurrency and availability for every product, with no plugin installed and nothing to configure.
- Confirm it's already running. View source on any WooCommerce product page and search for
application/ld+json. If WooCommerce is active and untouched, the block is there. - Check the values match your store settings.
priceCurrencyfollows WooCommerce → Settings → General → Currency, andavailabilityfollows the product's stock status field, so a currency change or a restock updates the schema automatically the next time the page renders. - Watch for minification stripping the tag. A caching or HTML-minification plugin configured aggressively can strip
<script>tags it doesn't recognise as critical. If the schema disappears from a live page but is present with caching disabled, exclude JSON-LD scripts from your minifier's rules. - Recurring billing on a WooCommerce Subscriptions product needs the
priceSpecification/billingDurationblock added by hand, in a Custom HTML block or a small theme filter on the product template, following Path 1's steps 3 to 5. Core WooCommerce covers one-time pricing; the recurring-billing detail is not automatic.
Shopify does the equivalent through its own theme filter rather than a core class, and that mechanism is already documented in full, with the exact file and line number, in The complete AI readiness setup for Shopify. If you're on Shopify, read that post's M4 section rather than re-deriving it here; the short version is that Dawn's {{ product | structured_data }} filter emits the same price/priceCurrency/availability fields for you.
If your WordPress site isn't running WooCommerce, don't assume your SEO plugin covers this. ThinkRank, the plugin we recommend first on WordPress for agent-facing SEO, handles Organization, Person and WebSite schema types out of the box; checked first-party against a live install's get-schema-settings on 22 September 2026, its enabled schema list does not include Product or Offer. A non-WooCommerce WordPress pricing page still needs Path 1's manual JSON-LD block, in a Custom HTML block or the page template, full stop.
Two platforms worth knowing the limits of
Not every builder treats structured data the same way, and two are worth a callout rather than a full path.
| Platform | What it allows | What that means for a pricing page |
|---|---|---|
| Wix | JSON-LD only, up to 5 markups per page, each under 7,000 characters, static pages only | A large plan catalogue can't be split across dynamic pages the way it could on WooCommerce or Shopify; keep pricing on one static page and one compact block |
| Framer | Custom Code panel accepts a {{Field}} CMS variable piped through json for CMS-driven values, plus an unsafeRaw escape hatch Framer itself flags as risky | Works well for a CMS-backed plans collection; use unsafeRaw sparingly, and validate the rendered output rather than the template |
Both facts are still-live as recorded in the queue notes for this item; if either becomes the main path for a specific reader rather than background color, re-verify it against the platform's current documentation before publishing.
How to verify it worked
Lead with AIScan, because it checks the same field the schema is meant to satisfy in one pass.
- Run
npx aiscan-cli yoursite.com/pricing, or paste the URL at aiscan.site. No account needed. - Read check M4 directly in the result. A pass means an
OfferorAggregateOfferblock was found withprice,priceCurrencyandavailabilityall present. - If M4 still fails after you've added the block, re-fetch the live page yourself; a script tag that renders in your editor but not in the deployed HTML (a caching layer, a build step that strips inline scripts) is the single most common cause.
If you'd rather check by hand, two methods work without any tool:
- Google's Rich Results Test. Paste your pricing URL in and look for the
OfferorProducttype in the parsed results. It flags malformed JSON before search engines or agents ever see it. curlplus a JSON-LD parse.curl -s https://yoursite.com/pricing | grep -o '<script type="application/ld+json">.*</script>'pulls the raw block out of the page; paste the extracted JSON into any online JSON validator to confirm it parses, then check theprice,priceCurrencyandavailabilityfields by eye against what the page actually shows.
Where AIScan fits, and where it doesn't
A scan confirms the block exists and carries the required fields. It can't confirm the number inside it is current, because that's a business fact, not a markup fact: a JSON-LD price of 12.00 next to a page that now charges 15.00 still passes M4, because the check reads structure, not truth. Keeping the structured price and the displayed price in sync is on you, on whatever process already updates the visible price.
Fix this, then check the rest
If your pricing page has no Offer or AggregateOffer block at all, Path 1's six steps are a same-day fix on any platform, and Path 2 means you may have nothing to do if you're already on WooCommerce. Run the scan afterward and read check M4 by name rather than trusting the page looks right. The rest of the commerce dimension, and every other check this series has covered, is indexed at /docs/checks/commerce and /guides.
Frequently asked questions
Why doesn't Google show my price in search results even though I added schema?
Rich results are Google's choice, not a guarantee tied to valid markup. A correct Offer block makes your price eligible for rich results; it doesn't force them to appear. Confirm the block itself is valid first with Google's Rich Results Test, then treat the visual result as a separate, unguaranteed outcome. If the test shows no errors and the price still doesn't show, that's Google's display decision, not a schema bug.
My AIScan scan still fails check M4 after I added the JSON-LD block. What's wrong?
The most common cause is a script tag that renders in your editor or CMS preview but not in the deployed page. Fetch the live URL directly, for example with curl, and search the raw HTML for application/ld+json. If it's missing there, a caching layer, a minification plugin, or a build step is stripping it before the page ships, even though your source file looks correct.
Should I use Offer or AggregateOffer on my pricing page?
Use Offer for one fixed price on one thing. Use AggregateOffer when a single product or plan spans a range, several tiers or variants at different prices, since it lets one block cover lowPrice and highPrice instead of forcing a separate Offer for every variation.
Do I need a plugin to get Offer schema on WordPress?
Only if you're not running WooCommerce. WooCommerce emits Offer schema for every product automatically, in its own class-wc-structured-data.php file, with nothing installed beyond WooCommerce core. On a non-WooCommerce WordPress pricing page, an SEO plugin's schema module usually covers Organization or WebSite types, not Product or Offer, so a hand-authored JSON-LD block in a Custom HTML block is still the fix.
Does Shopify already handle this, or do I still need to add anything?
Dawn and most current Shopify themes already emit Offer schema through the structured_data filter on the product template. Confirm it's present by viewing source on a product page and searching for application/ld+json; if your theme is customized and the filter was removed, add it back rather than writing a JSON-LD block from scratch.
What format should billingDuration use, a number or an ISO 8601 duration?
Schema.org accepts both. A UnitPriceSpecification can carry billingDuration as a plain number paired with a unitCode such as MON for month, or as an ISO 8601 duration string like P1M for one month or P1Y for one year. Both parse correctly; the only rule is to stay consistent across every Offer block on your own site so a script reading your pages doesn't have to branch on format.
Error: the price in my JSON-LD doesn't match what's on the page. Does that matter?
Yes, and it's worth fixing immediately rather than leaving it. A scan checks that the fields exist and parse; it can't check that the number is current. An agent that trusts your structured data will quote the stale figure to a shopper, which is a worse outcome than having no schema at all. Update the price in both places from the same source whenever it changes, ideally the same template variable feeding both.
Can I add pricing schema on Wix, and what are its limits?
Yes, through Wix's Custom Code panel, but within real limits: JSON-LD only, up to five markups per page, each under 7,000 characters, and static pages only, not dynamically generated ones. A short catalogue fits comfortably; a large one needs to be split or consolidated onto a single static pricing page rather than spread across pages Wix generates dynamically.
Related guides
How to ship one h1, title, meta description and JSON-LD on WordPress
WordPress sites fail AIScan's C3 check more often than their owners expect, and almost never for the reason they expect. Across the 129 WordPress sites in our scan corpus, verified on 8 September…
The Agentic Commerce Stack in 2026: UCP, ACP, AP2 and x402, Measured on 30 Storefronts
Five protocol names now compete for the same sentence in every agentic commerce explainer: UCP, ACP, AP2, x402 and MPP. Almost every explainer treats them as rivals. They are not. Four of the five…
Google AI Overviews in 2026: What You Can Actually Control, Measured on 110 Publishers
Search Console will tell you that AI Overviews sent you traffic. It will not tell you how to turn them off, because there is no setting to turn off. Google publishes exactly four controls that reach…
Cloudflare Pay Per Crawl in 2026: Should You Charge AI Crawlers? 119 Sites Measured
Stack Overflow will sell you its homepage for fifty cents. Point an AI crawler at it and the server answers HTTP 402 Payment Required, crawlerprice: USD 0.5, and a JSON body naming Cloudflare's Pay…
