Schema.org can express much of what a SaaS pricing model contains: the duration a price applies for, per-unit rates, minimum quantities, price components that apply in parallel, promotional windows, tax treatment and regional eligibility. Google documents consuming a much smaller set than that. The gap is not a reason to skip the markup, but it is a reason to be precise about which fields are documented, which are semantically available and undocumented, and which are recently added to the vocabulary. Some of it cannot be expressed at all - there is no clearly documented property for how often a customer is actually charged. Everything that matters commercially - the payment cadence, the minimum, the overage rate, what the trial converts to - has to survive in the visible content regardless, because that is the only layer every reader and every system can see.
Two labels mark evidence boundaries on this page. Documented means the guidance that follows is directly supported by published Schema.org or Google documentation, quoted. Recommendation means a modelling choice or workflow that CoreAEX prescribes and the documentation does not. Untagged text is ordinary explanation or a conclusion following from something already labelled.
Model the Commercial Truth First
Subscription markup goes wrong when someone starts from the JSON-LD and works backwards to the pricing. Write the facts down first, in plain language, and the markup decisions mostly settle themselves.
A SaaS price is rarely one number. It is usually a set of facts that a single price field cannot hold:
| Fact | Question it answers | Example |
|---|---|---|
| Amount | How much is charged? | 780 |
| Currency | In what currency? | USD |
| Unit | Per what? | Per user |
| Cadence | How often is it charged? | Once every 12 months |
| Commitment | What is the customer bound to? | 12-month term |
| Minimum quantity | What is the smallest purchase? | 5 seats |
| Included allowance | What is bundled before extra charges? | 2,000,000 events/month |
| Overage rate | What happens past the allowance? | $1.50 per 1,000 events |
| One-off fees | What is charged once, not recurring? | Onboarding fee |
| Tax treatment | Is the displayed price tax-inclusive? | Excludes VAT |
| Eligibility | Who and where can buy this? | Business customers, EU only |
| Validity | When does this price start and stop? | Promotional, September only |
Recommendation Write this table out for each plan before opening an editor. Two things fall out of it immediately. The first is which facts your visible page is currently hiding - a minimum seat count in a footnote, a setup fee only revealed at checkout. The second is which facts have no home in the markup, which is a larger set than most implementations assume.
Documented A fact with no documented markup home is not a fact you can drop. It moves to the visible content, where it should have been anyway. Google's structured data guidelines are unambiguous that markup follows visible content rather than substituting for it: Don't mark up content that is not visible to readers of the page
.
What Google Documents Consuming, Property by Property
This is the reference the rest of the page depends on. Three distinct layers get conflated in most published guidance, and they have different answers:
- Schema.org core - the property exists in the released vocabulary and is stable.
- Schema.org new - the property exists in the released core vocabulary, added recently enough that Schema.org invites implementation feedback before treating its definition as settled. It is not a staging area for unreleased terms; it ships in the same release as everything else.
- Google-documented - Google's Search structured data documentation states it reads the property for a named feature.
A property can be core and undocumented, new and undocumented, or documented and narrow. Almost nothing a SaaS subscription needs is documented and broad.
| Pricing dimension | Schema.org property | Vocabulary status (v30.0) | Documented by Google Search? |
|---|---|---|---|
| Amount | price, or priceSpecification.price | Core | Yes |
| Currency | priceCurrency | Core | Yes - ISO 4217, three letters |
| Original price in a sale | priceType | Core (its enumeration is new) | Yes - StrikethroughPrice; see the note below on ListPrice |
| Sale window | validFrom with validThrough or priceValidUntil | Core | Yes |
| Quantity a price refers to | referenceQuantity | Core | Yes - "The quantity of the product offered for the given price" |
| Unit of measurement (code) | unitCode | Core | Yes - inside the QuantitativeValue for referenceQuantity, in Google's unit-pricing example |
| Unit of measurement (text) | unitText | Core | Not present in Google's unit-pricing pattern |
| Duration a price or component applies for | billingDuration | New | No documentation located |
| Payment recurrence / renewal frequency | No clearly documented property in the vocabulary | No documentation located | |
| Delay before a price becomes valid and billing begins | billingStart | New | No documentation located |
| Rounding / billing increment | billingIncrement | Core | No documentation located |
| Parallel price components | CompoundPriceSpecification, priceComponent | Core | No documentation located for the type |
| Component role (subscription, activation fee…) | priceComponentType | New | No documentation located |
| Minimum or maximum quantity | eligibleQuantity | Core | No documentation located |
| Minimum spend | eligibleTransactionVolume | Core | No documentation located |
| Add-ons | addOn | Core | No documentation located |
| Customer type eligibility | eligibleCustomerType | Core | No documentation located |
| Regional eligibility | eligibleRegion | Core | No documentation located |
| VAT treatment | valueAddedTaxIncluded | Core | No documentation located |
| Included allowance before overage | No dedicated, unambiguous property | No documentation located | |
| Free trial as a whole offer | No dedicated property; billingStart covers one part | No documentation located | |
"No documentation located" means a deliberate search of Google's Search structured data documentation found no statement that Google consumes the property. Per this cluster's evidence rules, that is an absence of located documentation - not evidence that the property has no effect, and not a reason to remove it if it accurately describes your pricing.
Documented Google does document UnitPriceSpecification, which surprises people who assume complex pricing is entirely undocumented. What it documents is a narrow slice: the merchant listing property table lists price and priceCurrency as required, with priceType, referenceQuantity and membershipPointsEarned recommended. The duration and increment properties - billingDuration, billingIncrement, billingStart - and unitText do not appear in that table. unitCode is the exception worth noting: it is absent from the property table but present in Google's worked unit-pricing example, nested inside the QuantitativeValue for referenceQuantity. Documentation status here is a question of where you look, not just whether a property is named.
Documented Two documented behaviours are worth committing to memory before you write anything, because both change what your markup does.
Google states a precedence rule between price and priceSpecification. Verbatim: If you use both the offers.price and offers.priceSpecification properties to encode an active price, Google will use the price provided through the offers.price property and ignore the offers.priceSpecification property.
So a detailed UnitPriceSpecification carrying an active price sitting beside a simple price is not additive - for Google's purposes the detailed one is ignored.
priceType's two supported values are documented on the same page, at different points in a transition. Google's detailed property table states: You must set the priceType to the https://schema.org/StrikethroughPrice value (no other values are supported).
The pricing overview on the same page explains why a second value still appears in practice: Strikethrough prices set the priceType property to StrikethroughPrice (for a transition period, ListPrice is also allowed)
. Read together, the two sentences describe one policy rather than two competing ones - StrikethroughPrice is the required, forward value, and ListPrice is a legacy value Google is still accepting while sites migrate off it. Schema.org's PriceTypeEnumeration has eight members - InvoicePrice, ListPrice, MSRP, MinimumAdvertisedPrice, RegularPrice, SRP, SalePrice and StrikethroughPrice - and Google names two of them, one as the durable answer and one as a transitional carve-out. The practical answer is unaffected: use StrikethroughPrice. Don't build a validation rule that rejects ListPrice outright, since Google's own documentation still accepts it for now, but treat it as the value being phased out rather than an equally valid ongoing choice.
Tiered Plans
A plan tier is a set of commercial terms, so it belongs in an Offer. The type-selection decision above it - whether the offers hang off a Product, a SoftwareApplication or a Service, and when a tier is a separate product rather than an offer - is settled on the type-selection page in this cluster and is not repeated here.
What this page adds is what goes inside each offer. Give every tier its own name, its own url pointing at that plan's anchor or signup path, its own price and priceCurrency, and a seller. The name and URL are what preserve plan identity; without them a set of offers is a set of numbers with no way to tell which is which.
Recommendation Resist the urge to encode the feature list. There is no property that expresses "includes SSO," and improvising one - stuffing entitlements into description, or inventing a property name - produces markup that is either ignored or wrong. Feature differences belong in the visible comparison table, which is where buyers read them anyway.
Monthly and Annual Billing
This is where most SaaS markup becomes inaccurate, and the cause is a display convention. Pricing pages routinely show an annual plan as a monthly figure - "$65/user/month, billed annually" - because it compares better against the monthly plan. The number the customer is actually charged is $780 once a year.
Recommendation Mark up the amount that is charged, not the amount that is displayed for comparison. If the annual offer's price is 65, the markup says the plan costs $65, which is not true of any transaction that will ever occur. Encode 780 as the price, and let the visible page carry the monthly equivalent where it helps the reader.
Recommendation Model monthly and annual as two separate offers, not one offer with two prices. They have different amounts, different commitments, different signup URLs and often different cancellation terms - which is to say they are different commercial terms, which is what an Offer is for. Two offers also keeps you clear of the precedence rule above, which applies when price and priceSpecification both encode an active price on the same offer. Whether a single offer carrying one cadence in price and another in a specification meets that description is not something Google's documentation settles - and the way to avoid finding out is to give each cadence its own offer, where each price is unambiguously the active one.
Cadence and duration are different facts, and only one of them has a property. This distinction is easy to lose, and losing it produces markup that looks precise and says something you did not mean.
Documented billingDuration on a UnitPriceSpecification is defined as: Specifies for how long this price (or price component) will be billed. Can be used, for example, to model the contractual duration of a subscription or payment plan. Type can be either a Duration or a Number (in which case the unit of measurement, for example month, is specified by the unitCode property).
Read that carefully: it is how long the price applies, not how often the customer is charged. A 12-month commitment paid monthly and a 12-month term paid once up front are commercially different arrangements, and both could be described as lasting twelve months.
So billingDuration: 12 does not mean "billed annually," and billingDuration: 1 does not mean "billed monthly." Use the property for the duration it actually describes - the term the price applies for - and only when your offer genuinely has that term. Schema.org provides no clearly documented property for payment recurrence. How often the card is charged, and whether the subscription renews, live in the visible copy. Keep amount, payment cadence and contractual commitment as three separate facts there, because no combination of markup will distinguish them.
billingDuration is marked "new" in Schema.org's current vocabulary release, not "pending" - it is part of the released core vocabulary rather than the separate pending.schema.org staging extension - and no Google documentation reviewed for this page states that Google reads it. Including it, correctly scoped, describes your pricing accurately; it does not buy a search feature.
Per-Seat Pricing
Per-seat pricing needs two facts that a bare price cannot carry: what the price is per, and what the smallest purchase is.
Documented The "per" is referenceQuantity, a QuantitativeValue on the UnitPriceSpecification. Schema.org defines it as The reference quantity for which a certain price applies, e.g. 1 EUR per 4 kWh of electricity.
This is the one complex-pricing property in this section that Google does document, though its definition there is bare: The quantity of the product offered for the given price.
Recommendation Be careful about what that documentation covers. Google's documented use of unit pricing is oriented to physical measures - weight, volume, length, area, and counts of items. No documented unit represents a seat, a user or a licence. Expressing "per user" through referenceQuantity is a reasonable semantic reading of the property, and it is an extrapolation beyond what Google documents rather than a supported pattern.
Documented The unit itself goes in unitCode, unitText, or both, and these two have different documentation status. Schema.org says unitCode takes the UN/CEFACT Common Code (3 characters) or a URL
, and describes unitText as A string or text indicating the unit of measurement. Useful if you cannot provide a standard unit code for unitCode.
unitCode does appear in Google's documentation - not in the UnitPriceSpecification property table, but nested inside the QuantitativeValue in Google's worked unit-pricing example, where a price per 100 ml is expressed with a unitCode of "ML". unitText does not appear in that pattern. So the honest split is: unitCode is documented as part of Google's unit-pricing shape; unitText is valid core vocabulary that Google's unit-pricing example does not use.
The codes themselves are verifiable. UN/CEFACT's published vocabulary gives C62 as "one" and, in its duration vocabulary, MON as "month" and ANN as "year". What that does not establish is that C62 paired with a unitText of "user" or "events" is read by anything as a seat or an event. Google's documented unit-pricing measures are physical - volume, weight, length - and a seat is not among them. The code is correct; the SaaS reading of it is ours.
The minimum is eligibleQuantity, defined as The interval and unit of measurement of ordering quantities for which the offer or price specification is valid.
A minValue of 5 says the offer applies from five seats up. Core vocabulary, no documented Google consumer. It matters anyway: a "$25 per user" plan with a five-seat floor has a real entry price of $125, and a page that shows only the $25 is describing a price nobody pays.
Usage-Based Pricing
Consumption pricing usually has three parts: a base fee, an included allowance, and a rate beyond it. Google documents none of this. Schema.org can express most of it.
Documented The base fee and the overage rate are two prices that apply in parallel, which is what CompoundPriceSpecification is for: A compound price specification is one that bundles multiple prices that all apply in combination for different dimensions of consumption. Use the name property of the attached unit price specification for indicating the dimension of a price component (e.g. 'electricity' or 'final cleaning').
That last sentence is the practical instruction: the name on each component is how you label what the component is for.
The overage rate uses referenceQuantity for the block it is quoted in - $1.50 per 1,000 events.
Recommendation billingIncrement is not a second way of saying "per 1,000." Schema.org defines it as This property specifies the minimal quantity and rounding increment that will be the basis for the billing. The unit of measurement is specified by the unitCode property.
That is a charging rule: it states that billing is calculated in the specified increments. The definition does not, by itself, establish how partial increments are rounded. Only use it if your billing genuinely works in increments and your visible copy says so - and give that specification its own unitCode, which is where Schema.org itself says the unit belongs. If usage is prorated continuously, leave it out; referenceQuantity already expresses the rate. Inferring an increment-based charging rule from the way a rate is written is not something the display wording supports.
Recommendation The included allowance has no dedicated, unambiguous property. There is no vocabulary term that means "2,000,000 events included before overage begins." eligibleQuantity can express a quantity interval over which a price specification is valid, so a threshold can be modelled with it - but that is a modelling interpretation applied to consumed usage rather than ordering quantities, and it still does not state that the quantity below the threshold is included at no extra charge. Treat it as a choice you are making, not a rule the vocabulary supplies.
Either way the allowance has to appear in the visible pricing content. This is the clearest case on the page of a commercial fact that markup cannot carry cleanly, and it is a material one: an answer that reports "$500 per month" without the allowance and the overage rate has reported a number that is true and an impression that is false.
Compound Pricing
Setup fees, onboarding charges, platform fees and per-unit rates that all apply together are the general case of the section above. CompoundPriceSpecification holds them; priceComponent links each one; each component is a UnitPriceSpecification with its own name, amount and any applicable duration or unit information. Payment cadence must remain explicit in the visible content.
Documented Schema.org has a vocabulary for what kind of component each one is: priceComponentType, whose enumeration - PriceComponentTypeEnumeration - includes Subscription, ActivationFee, Installment, Downpayment, CleaningFee and DistanceFee. ActivationFee is defined as Represents the activation fee part of the total price for an offered product, for example a cellphone contract
- a good semantic fit for a SaaS onboarding fee. Both the property and every member of its enumeration are marked "new" in Schema.org's vocabulary - already part of the released core, recent enough that Schema.org is still inviting implementation feedback on their definitions - and no Google documentation for either was located.
Recommendation Whether to use a recently added term is a judgement rather than a rule. They describe your pricing more precisely than the alternatives, they are valid vocabulary today, and Schema.org's own framing for its "new" area is that implementation feedback and adoption can still help refine the definitions. Use them where they genuinely fit, keep a record of which newer terms your templates rely on, and re-check them when Schema.org publishes a release. Do not use them in the expectation of a search feature, because none is documented.
Free Plans, Trials and Promotional Pricing
Three different things that get encoded the same way and should not be.
Documented A free plan is price: 0. Google documents this directly for product snippets, which permit a zero price, and for the Software app feature: If the app is available without payment, set offers.price to 0
. Note the boundary: merchant listing experiences do not accept it, because Unlike product snippets, merchant listing experiences require a price greater than zero.
A free trial is not price: 0. A 14-day trial of a $79 plan is a $79 plan with a promotional entry condition. Encoding it as a zero-price offer states that the product is available without payment, which stops being true on day 15.
There is no dedicated trial property in the vocabulary, but that is not the same as nothing being available. billingStart is defined as Specifies after how much time this price (or price component) becomes valid and billing starts. Can be used, for example, to model a price increase after the first year of a subscription.
That can express one part of a trial arrangement - the delay before the paid price takes effect. It is marked "new" in Schema.org's vocabulary, no Google documentation for it was located, and it describes only the billing delay: not access during the trial, not whether a card is required, not the cancellation terms, not what happens at conversion.
Recommendation So the trial length, the conversion price, the first billing date, the card requirement and the cancellation terms belong in the visible content, described in words, with the markup describing the plan the trial converts into at its real price. What you must not do is represent the whole paid offer as permanently price: 0.
Documented A promotional price is a sale price, and this is the one part of this section Google documents in detail. The documented pattern puts the current, active price on the Offer and the original price in a UnitPriceSpecification marked with priceType: The current, active price automatically becomes a sale price when you provide a second price with the original, strikethrough price and mark it with a priceType property of value https://schema.org/StrikethroughPrice.
Google states the corresponding prohibition twice, in slightly different wording each time: Don't mark the active price with a priceType property.
Elsewhere on the same page, next to the property table, the parallel instruction reads Don't mark the current sale price with the priceType property.
The sale window is documented too, and it carries an asymmetry worth knowing. Google's guidance is to use validFrom for the start and either validThrough or priceValidUntil for the end, in ISO 8601 format, and to include the time and timezone. validFrom and validThrough are documented as working on both the Offer node and a PriceSpecification node, but: Note that the priceValidUntil property isn't applicable to the PriceSpecification type.
So priceValidUntil goes on the Offer only, while validFrom and validThrough work in both places.
Documented A promotional price with no end date is the failure mode here. Google's general guidelines require current information - Provide up-to-date information. We won't show a rich result for time-sensitive content that is no longer relevant
- and a discount that ended in March, still marked up in August, is that. Set the end date when you set the price, not afterwards.
Enterprise and Contact-Sales Plans
The rule is short and admits no exceptions: do not encode a price you do not publish. Not 0, not a placeholder, not a guessed range, not the lowest price you have ever agreed. An Offer with a name, a URL, a seller and no price is valid vocabulary and an accurate description of a quote-led plan.
What you can encode honestly is the shape of the deal where it is publicly stated: a documented seat minimum through eligibleQuantity, a minimum spend through eligibleTransactionVolume - The transaction volume, in a monetary unit, for which the offer or price specification is valid, e.g. for indicating a minimal purchasing volume
- or a customer-type restriction through eligibleCustomerType. All core vocabulary, none with located Google documentation, all only appropriate when the page visibly says the same thing.
Leave availability out unless the page communicates a real, maintainable status. It is an assertion about a commercial state, not a completeness field, and asserting one for a plan that only exists after a sales conversation is a guess.
Currencies, Regions and Taxes
Documented Google's guidance on multiple currencies is a URL-structure requirement rather than a markup one: When offering products for sale in multiple currencies, have a distinct URL per currency. For example, if a product is available for sale in Canadian and US dollars, use two distinct URLs, one per currency.
Currency values are ISO 4217, three letters.
Recommendation That has a consequence for the common SaaS pattern of switching currency with a client-side toggle on one URL. Under that pattern there is no distinct URL per currency, so the documented structure is not met, and whichever currency renders server-side is the one the markup should state. The toggle is a usability feature; it is not a way to publish four currencies at one address.
Tax treatment has a property and no documented consumer. valueAddedTaxIncluded is a boolean on PriceSpecification: Specifies whether the applicable value-added tax (VAT) is included in the price specification or not.
For B2B SaaS sold across borders this is one of the more consequential facts on the page and one of the least often stated. Whether or not anything reads the property, the visible page needs to say which it is.
Regional eligibility uses eligibleRegion, which takes an ISO 3166-1 or 3166-2 code, a Place or a GeoShape. Its counterpart ineligibleRegion exists too, marked "new" in the same way as eligibleRegion itself - it is not a separate, less-finished tier.
A Boundary Worth Drawing: Search Markup Is Not a Product Feed
Anyone researching SaaS subscription pricing in Google's documentation will hit Merchant Center, and it is a different system with different rules. Blending the two is the most consequential error available on this topic, so it is worth stating where the line falls.
Documented Merchant Center has feed attributes for subscription pricing - subscription_cost, with sub-attributes for period, period length and amount. Software is carved out of it explicitly: Don't use the subscription cost attribute for software subscriptions. Software subscriptions must be prepaid, and submitted using the yearly prepaid cost as the price [price] attribute.
Google's best-practice page for advertising software subscriptions adds that a subscription must be available in prepaid, 12-month (yearly) increments with a duration of at least 1 year
and instructs merchants to Submit the price for the full length of the software subscription, with a minimum of 1 year.
Two things follow. Monthly SaaS billing has no representation in that system at all - the documented route is an annual prepaid figure. And none of it is on-page markup: these are feed attributes submitted to Merchant Center, and the software-subscriptions guidance makes no mention of Schema.org or structured data.
Documented On-page markup does reach Merchant Center, through two documented channels rather than one. Google states them together: Valid structured markup allows us to read your product data and enable 2 features: Automatic item updates: Automatic item updates reduce the risk of account suspension and item disapproval due to price and availability mismatches. Automated feeds: Available via the 'website crawl' input method to create a primary feed in your Merchant Center account.
The second is more than correction - Google will create a product data feed based on the structured data you've provided on your website
.
For automatic item updates, Google names the required set: Specifying the following schema.org values is required for automatic item updates: price, priceCurrency, availability and condition.
Note that "condition" there is the feed field name; the Schema.org property to implement in JSON-LD is itemCondition, which is what Google's own example on that page uses.
Documented This path has a delivery requirement that Google Search does not impose. Verbatim: Structured data markup must be present in the HTML returned from the web server. The structured data markup can't be generated with JavaScript after the page has loaded.
Google Search, by contrast, documents that it can process structured data available in the rendered DOM. If your pricing markup is client-side rendered, it may be readable by one of these systems and not the other - which is a concrete reason to render it server-side rather than a general principle.
Two further limits Google states: automatic item updates are not a replacement for providing regular updates to the Merchant Center product data
, and Your landing page cannot change based on information about the customer, such as if you adapt prices based on a customer's IP address or browser type.
That last one rules out a good deal of geo-personalised SaaS pricing.
All of this concerns advertised products in Merchant Center. None of it is a Search retrieval mechanism, and it should not be cited as evidence that search or AI systems read price markup.
Implementation Examples
Five patterns, each paired with the visible copy it describes. The pairing is the point: if the two ever disagree, the markup is wrong, not the page.
1. Tiered plan with monthly and annual billing
Visible copy: "Growth - $79 per user/month on a rolling monthly term, cancel any time. Or $780 per user prepaid for a 12-month term ($65 per user/month equivalent)."
{
"@context": "https://schema.org",
"@type": "Product",
"@id": "https://example.com/#northwind",
"name": "Northwind Analytics",
"url": "https://example.com/pricing",
"offers": [
{
"@type": "Offer",
"name": "Growth, billed monthly",
"url": "https://example.com/pricing#growth-monthly",
"price": 79,
"priceCurrency": "USD",
"seller": { "@type": "Organization", "name": "Northwind Software Ltd" },
"priceSpecification": {
"@type": "UnitPriceSpecification",
"price": 79,
"priceCurrency": "USD",
"billingDuration": 1,
"unitCode": "MON",
"referenceQuantity": {
"@type": "QuantitativeValue",
"value": 1,
"unitCode": "C62",
"unitText": "user"
}
}
},
{
"@type": "Offer",
"name": "Growth, billed annually",
"url": "https://example.com/pricing#growth-annual",
"price": 780,
"priceCurrency": "USD",
"seller": { "@type": "Organization", "name": "Northwind Software Ltd" },
"priceSpecification": {
"@type": "UnitPriceSpecification",
"price": 780,
"priceCurrency": "USD",
"billingDuration": 12,
"unitCode": "MON",
"referenceQuantity": {
"@type": "QuantitativeValue",
"value": 1,
"unitCode": "C62",
"unitText": "user"
}
}
}
]
}
The annual price is 780, not 65. The $65 figure is a comparison device on the visible page; the charge is $780.
billingDuration here states the term, not the payment frequency. The annual offer carries 12 months because that is the length of the prepaid term; the monthly offer carries 1 month because that offer genuinely runs on a rolling one-month term. Neither value says the charge repeats - that fact lives only in the visible copy, which is why the copy above states the term and the cancellation condition explicitly. If your monthly plan is actually a 12-month commitment paid monthly, billingDuration on it is 12, not 1, and the payment frequency still has nowhere to go.
billingDuration is marked "new," with no located Google documentation. Because each offer carries a simple price alongside its specification, Google's documented precedence rule applies - the price is what Google uses.
2. Per-seat pricing with a seat minimum
Visible copy: "Team - $25 per user/month on a rolling monthly term. Minimum 5 users."
{
"@type": "Offer",
"@context": "https://schema.org",
"name": "Team",
"url": "https://example.com/pricing#team",
"price": 25,
"priceCurrency": "USD",
"seller": { "@type": "Organization", "name": "Northwind Software Ltd" },
"eligibleQuantity": {
"@type": "QuantitativeValue",
"minValue": 5,
"unitCode": "C62",
"unitText": "user"
},
"priceSpecification": {
"@type": "UnitPriceSpecification",
"price": 25,
"priceCurrency": "USD",
"billingDuration": 1,
"unitCode": "MON",
"referenceQuantity": {
"@type": "QuantitativeValue",
"value": 1,
"unitCode": "C62",
"unitText": "user"
}
}
}
The five-seat floor makes the real entry price $125 a month. Nothing in the markup computes that, and no documented consumer reads eligibleQuantity, so the visible page has to state the minimum plainly rather than leaving it to a footnote.
3. Usage-based pricing with an allowance and overage
Visible copy: "Scale - $500 per month on a rolling monthly term, including 2,000,000 events. Additional events are billed at $1.50 per 1,000, prorated."
{
"@type": "Offer",
"@context": "https://schema.org",
"name": "Scale, usage-based",
"url": "https://example.com/pricing#scale",
"priceCurrency": "USD",
"seller": { "@type": "Organization", "name": "Northwind Software Ltd" },
"priceSpecification": {
"@type": "CompoundPriceSpecification",
"priceCurrency": "USD",
"priceComponent": [
{
"@type": "UnitPriceSpecification",
"name": "Platform fee",
"price": 500,
"priceCurrency": "USD",
"billingDuration": 1,
"unitCode": "MON"
},
{
"@type": "UnitPriceSpecification",
"name": "Events beyond the 2,000,000 included each month",
"price": 1.5,
"priceCurrency": "USD",
"referenceQuantity": {
"@type": "QuantitativeValue",
"value": 1000,
"unitCode": "C62",
"unitText": "events"
}
}
]
}
}
There is deliberately no price on the Offer here, because no single number is the price of this plan. The allowance survives only as text inside the component's name, which is what Schema.org suggests name is for on a price component, and it is not a structured value.
billingIncrement is deliberately absent. The visible copy says overage is prorated, so there is no rounding rule to assert. Had the copy said usage is billed in whole 1,000-event blocks, billingIncrement: 1000 would belong here - with a unitCode on this specification to give the increment its dimension, as Schema.org's own definition ties the two together. Adding it to match the way the rate is written would state a charging rule the page does not make.
No Google documentation was located for CompoundPriceSpecification, so treat this pattern as an accurate semantic description with no known search consumer.
4. Free plan
Visible copy: "Free - $0. Up to 3 users. Free forever, no card required."
{
"@type": "Offer",
"@context": "https://schema.org",
"name": "Free",
"url": "https://example.com/pricing#free",
"price": 0,
"priceCurrency": "USD",
"seller": { "@type": "Organization", "name": "Northwind Software Ltd" },
"eligibleQuantity": {
"@type": "QuantitativeValue",
"maxValue": 3,
"unitCode": "C62",
"unitText": "user"
}
}
This shape is correct only because the plan is genuinely free indefinitely. For a 14-day trial of a paid plan, there is no equivalent pattern: mark up the plan the trial converts into, at its real price, and describe the trial in the visible copy. A trial encoded as price: 0 is a false statement with a two-week shelf life.
5. Promotional price, using Google's documented sale pattern
Visible copy: "Growth annual - $780 $624 per user/year. Offer ends September 30, 2026."
{
"@type": "Offer",
"@context": "https://schema.org",
"name": "Growth, billed annually",
"url": "https://example.com/pricing#growth-annual",
"price": 624,
"priceCurrency": "USD",
"validFrom": "2026-09-01T00:00:00-04:00",
"priceValidUntil": "2026-09-30T23:59:59-04:00",
"seller": { "@type": "Organization", "name": "Northwind Software Ltd" },
"priceSpecification": {
"@type": "UnitPriceSpecification",
"priceType": "https://schema.org/StrikethroughPrice",
"price": 780,
"priceCurrency": "USD"
}
}
This is the one complex pattern on the page that follows a shape Google documents. The active price sits on the Offer and carries no priceType; the original price sits in the specification and is marked StrikethroughPrice. priceValidUntil is on the Offer, which is the only place Google documents it working. When the promotion ends, both the visible copy and this block have to change - put that in the campaign checklist rather than trusting anyone to remember.
How these were checked, and what was not checked
Each pattern was extracted from this page's finished HTML, parsed as JSON, and checked programmatically against the published Schema.org vocabulary release, version 30.0 (released March 19, 2026): every type confirmed to exist as a class, every property confirmed to exist and to be declared on the type carrying it, and every term checked for its release status - core, new, or pending. All five carry zero vocabulary errors. Patterns 1, 2 and 3 use one recently added property, billingDuration, disclosed above.
Three limits on that check, stated because the whole page turns on the distinction. It does not establish Google feature eligibility, which is assessed separately against each feature's documented required properties. It is not a Rich Results Test run - these patterns are illustrative and not deployed at a live URL. And it says nothing about whether a C62 code paired with a unitText of "user" is interpreted by any system as a seat - the codes are confirmed against UN/CEFACT's published vocabulary, but the SaaS reading of them is a modelling choice. Validation procedure, tooling and release gates are covered by the implementation page in this cluster.
What This Page Doesn't Settle
Which top-level type carries these offers. Whether your offers hang off a Product, a SoftwareApplication or a Service, and when a plan tier is better modelled as a separate product, is settled by the type-selection page in this cluster.
Documented How to deploy this without it drifting out of date. Generating markup and visible pricing from one source of truth, JSON-LD placement, server versus JavaScript delivery, validator coverage and release gates all belong to the implementation page. One documented caveat is worth carrying across in the meantime: Using Product markup? Be aware that dynamically-generated markup can make Shopping crawls less frequent and less reliable, which can be an issue for fast-changing content like product availability and price.
That is scoped to Shopping crawls and is conditional.
Whether any of this changes what AI answers say about your pricing. This page has established which properties Google documents consuming, which is a different question from what any system extracts in practice. The empirical question is the subject of original CoreAEX research in progress, and nothing above should be read as a claim about AI extraction or citation.
Pricing model that doesn't fit any of these shapes?
Hybrid seat-plus-usage billing, regional price lists, or a migration between pricing models are all worth talking through before the markup gets written. Book a session.
Sources
Sources:
Property definitions, ranges, domains and vocabulary status are from Schema.org, read from the published vocabulary release version 30.0 (released March 19, 2026) and confirmed against the term pages: UnitPriceSpecification, CompoundPriceSpecification, Offer, referenceQuantity, billingDuration, billingStart, billingIncrement, eligibleQuantity, eligibleTransactionVolume, valueAddedTaxIncluded, unitCode, unitText and ActivationFee. The "new" status for billingDuration, billingStart, priceComponentType, PriceComponentTypeEnumeration, ActivationFee, PriceTypeEnumeration and ineligibleRegion was confirmed on each term's own page, where Schema.org describes the "new" area as one where "implementation feedback and adoption from applications and websites can help improve our definitions" - distinct from the separate pending.schema.org staging area for genuinely unreleased terms.
Google feature requirements are from Google Search Central: Merchant listing structured data (reviewed 29 August 2026; a specific last-updated date could not be independently confirmed for this page), Product snippet (last updated December 10, 2025), Software app (last updated December 10, 2025), General structured data guidelines (last updated July 10, 2026), and JavaScript-generated structured data (last updated December 10, 2025).
Merchant Center material is separate from all of the above and is feed documentation, not on-page markup: subscription cost attribute, advertising software subscriptions, set up structured data for Merchant Center and about structured data markup for Merchant Center, which names automatic item updates and automated feeds as the two features valid markup enables.
Unit codes are from UN/CEFACT's published web vocabulary, which gives C62 as "one" in UnitMeasureCode and MON as "month" and ANN as "year" in DurationUnitMeasureCode. Google's unit-pricing example, which uses unitCode inside referenceQuantity, is on the merchant listing page above.
About the author
Zarko Zivkovic is the founder of CoreAEX, building technical SEO, AEO, and AI-visibility systems for B2B SaaS companies. Connect on LinkedIn.