Home › Blog › Shopify shipping structured data for AI shopping agents
Shopify shipping structured data for AI shopping agents: OfferShippingDetails, ShippingDeliveryTime, and free-shipping threshold schema
When a shopper asks ChatGPT Shopping "does this store offer free shipping on orders over $50?" or Perplexity "can I get this delivered by Thursday?", the AI agent's answer comes from structured data — not from reading your shipping policy page. The OfferShippingDetails and ShippingDeliveryTime schema types are the machine-readable layer AI agents use to answer those questions. Over 92% of Shopify stores have none of it.
OfferShippingDetails schema on product pagesContents
- Why AI shopping agents need shipping structured data
- OfferShippingDetails: anatomy of the schema
- ShippingDeliveryTime: separating handling from transit
- Free-shipping threshold: eligibleTransactionVolume
- Regional shipping: DefinedRegion for multi-market stores
- Liquid implementation for Dawn themes
- Five mistakes that break shipping schema
- Testing and validation
- FAQ
Why AI shopping agents need shipping structured data
Shipping is the third-most-common purchase intent modifier in AI shopping queries, after price ("under $50") and availability ("in stock"). Queries like "free shipping running shoes", "get a yoga mat delivered this week", and "cheapest laptop with next-day delivery" are answered by AI agents from a combination of Product JSON-LD and — critically — OfferShippingDetails schema.
Without shipping structured data, AI shopping agents have three options:
- Skip the product from shipping-filtered result sets entirely — the safest choice for an agent that cannot verify the claim.
- Parse the shipping policy page — unstructured text that varies wildly between stores and is often behind JavaScript or in PDF format. This produces unreliable, hard-to-normalise results.
- Make no claim — show the product in results but omit the "Free shipping" or "Arrives by Thursday" badge that influences click-through.
None of these outcomes serves you. Stores that do have OfferShippingDetails and ShippingDeliveryTime get the shipping badge in AI result cards and are included in shipping-filtered queries. That is a compounding advantage: shipping-filtered queries convert at 2× the rate of unfiltered browse queries because the shopper has already qualified the logistics constraint before clicking.
OfferShippingDetails for its AI Mode shopping cards (released Q1 2026). Google AI Mode uses the same structured data signals as Google Shopping — if you have Google Shopping set up but no OfferShippingDetails in your Product JSON-LD, you have a data gap between your feed and your organic schema that Google's AI Mode resolver has to bridge on every crawl.
OfferShippingDetails: anatomy of the schema
OfferShippingDetails is a schema.org type that lives inside an Offer object as the shippingDetails property. It describes a single shipping option — one rate, one region, one delivery window. For stores with multiple shipping options (standard + express, or free + paid), you use multiple OfferShippingDetails objects inside the same Offer.
MonetaryAmount with value (numeric) and currency (ISO 4217). For free shipping, set value: 0. Do not omit — missing shippingRate means AI agents cannot answer the "how much to ship?" query.DefinedRegion specifying where this shipping option applies. At minimum, set addressCountry to an ISO 3166-1 alpha-2 code. For US stores shipping domestically, use "US". For Shopify Markets stores, create one OfferShippingDetails per region.ShippingDeliveryTime object that breaks delivery into handlingTime (warehouse processing) and transitTime (carrier in transit). Both are QuantitativeValue ranges in business days.PriceSpecification with minPrice set to the cart minimum that unlocks this shipping rate. Used by AI agents to answer "how much to spend for free shipping?" queries.true for digital products (downloads, gift cards, subscriptions) that have no physical shipping. AI agents exclude products with doesNotShip: true from "needs delivery" filtered queries.Here is the minimal valid OfferShippingDetails for a US-only free shipping offer — the skeleton everything builds on:
{
"@type": "OfferShippingDetails",
"shippingRate": {
"@type": "MonetaryAmount",
"value": 0,
"currency": "USD"
},
"shippingDestination": {
"@type": "DefinedRegion",
"addressCountry": "US"
}
}
ShippingDeliveryTime: separating handling from transit
The deliveryTime property is where most implementations fall short. Merchants often write a single "3–5 business days" estimate in their shipping policy — but AI shopping agents (and Google's merchant experience) distinguish between two phases of that window:
- Handling time (
handlingTime): how long between order placement and the package leaving your warehouse. For same-day fulfilment cut-off stores, this is 0–1 business days. For stores that batch-process orders, it may be 1–3 days. - Transit time (
transitTime): how long the carrier takes to deliver once the package is collected. For standard USPS Ground Advantage in the US, this is typically 2–8 business days depending on origin/destination distance.
Separating these matters because the AI agent's "can I get this by Friday?" calculation requires knowing both numbers separately. An agent that sees a combined 3–5 days window cannot tell whether the store ships today (handling: 0) with a 3–5 day carrier, or whether they take 3 days to ship (handling: 3) and use overnight carrier delivery (transit: 1). The first scenario can answer "yes" to a Friday delivery query on a Monday; the second cannot.
The full ShippingDeliveryTime structure with both components:
{
"@type": "ShippingDeliveryTime",
"handlingTime": {
"@type": "QuantitativeValue",
"minValue": 0,
"maxValue": 1,
"unitCode": "DAY"
},
"transitTime": {
"@type": "QuantitativeValue",
"minValue": 3,
"maxValue": 7,
"unitCode": "DAY"
},
"cutoffTime": "16:00:00-05:00"
}
The optional cutoffTime property (ISO 8601 time with timezone offset) tells AI agents and Google's merchant experience the order deadline for same-day processing. A cutoffTime of 16:00:00-05:00 means orders placed before 4 PM US Eastern time begin processing the same business day. This is used to answer "get it by tomorrow" queries correctly — an order placed at 2 PM ET on Monday, with a 0–1 day handling time, can legitimately promise Tuesday + transit.
"unitCode": "DAY" (UN/CEFACT DAY unit code) rather than "unitText": "days". The unitText property is human-readable only; AI agents and Google's schema parser require the standardised unitCode for machine-interpretable delivery time calculations.
Free-shipping threshold: eligibleTransactionVolume
Most Shopify stores offer free shipping above a cart threshold — typically $35, $50, or $75 in the US market. This is one of the highest-leverage shipping schema implementations because "free shipping over $X" is a heavily searched purchase qualifier in AI shopping interfaces.
The eligibleTransactionVolume property on OfferShippingDetails lets you declare a minimum order value that activates a specific shipping rate. Combined with a zero-cost shippingRate, this expresses your free shipping threshold to AI agents in fully machine-readable form.
The correct approach is two OfferShippingDetails blocks in the same Offer: one for the paid rate (no minimum), and one for the free rate (with the threshold):
"shippingDetails": [
{
"@type": "OfferShippingDetails",
"shippingRate": {
"@type": "MonetaryAmount",
"value": 5.99,
"currency": "USD"
},
"shippingDestination": {
"@type": "DefinedRegion",
"addressCountry": "US"
},
"deliveryTime": {
"@type": "ShippingDeliveryTime",
"handlingTime": {
"@type": "QuantitativeValue",
"minValue": 0,
"maxValue": 1,
"unitCode": "DAY"
},
"transitTime": {
"@type": "QuantitativeValue",
"minValue": 3,
"maxValue": 7,
"unitCode": "DAY"
}
}
},
{
"@type": "OfferShippingDetails",
"shippingRate": {
"@type": "MonetaryAmount",
"value": 0,
"currency": "USD"
},
"shippingDestination": {
"@type": "DefinedRegion",
"addressCountry": "US"
},
"eligibleTransactionVolume": {
"@type": "PriceSpecification",
"minPrice": 50,
"priceCurrency": "USD"
},
"deliveryTime": {
"@type": "ShippingDeliveryTime",
"handlingTime": {
"@type": "QuantitativeValue",
"minValue": 0,
"maxValue": 1,
"unitCode": "DAY"
},
"transitTime": {
"@type": "QuantitativeValue",
"minValue": 3,
"maxValue": 7,
"unitCode": "DAY"
}
}
}
]
With this in place, an AI shopping agent can answer: "This store charges $5.99 shipping on orders under $50, and offers free standard shipping (3–7 business days) on orders of $50 or more." That answer is surfaced as a badge in AI result cards for queries like "free shipping yoga mat" without any user clicking through to your site.
eligibleTransactionVolume threshold should match your actual Shopify free shipping rate setting, not a marketing-copy version. If your Shopify shipping profile says "Free standard shipping on orders over $49.99", your minPrice should be 49.99, not 50. Mismatches are caught by Google's merchant feed validator and can cause your shipping badge to be suppressed in Google AI Mode.
Regional shipping: DefinedRegion for multi-market stores
For stores using Shopify Markets or shipping to multiple countries, DefinedRegion gives you precise control over which shipping options apply to which geographies. AI shopping agents use shippingDestination to filter out products that do not ship to a user's detected or stated location.
The DefinedRegion type supports three granularity levels:
| Level | Properties | Example use case |
|---|---|---|
| Country | addressCountry (ISO alpha-2) |
Domestic US shipping, or blanket "ships to UK" |
| State/province | addressCountry + addressRegion (ISO 3166-2 subdivision) |
In-state only offers, or BOPIS restricted to CA/NY |
| Postal code range | addressCountry + postalCodeRange with postalCodeBegin / postalCodeEnd |
Same-day delivery metro area; local delivery radius |
For a Shopify Markets store shipping to the US, UK, and EU with different rates and delivery windows, the correct structure is one OfferShippingDetails per market, each with its own shippingRate, shippingDestination, and deliveryTime. The shippingDetails array accepts any number of objects, so there is no structural limit to how many regions you can declare.
A common pattern for a two-market store (US standard + International):
"shippingDetails": [
{
"@type": "OfferShippingDetails",
"shippingRate": { "@type": "MonetaryAmount", "value": 0, "currency": "USD" },
"shippingDestination": { "@type": "DefinedRegion", "addressCountry": "US" },
"eligibleTransactionVolume": { "@type": "PriceSpecification", "minPrice": 50, "priceCurrency": "USD" },
"deliveryTime": {
"@type": "ShippingDeliveryTime",
"handlingTime": { "@type": "QuantitativeValue", "minValue": 0, "maxValue": 1, "unitCode": "DAY" },
"transitTime": { "@type": "QuantitativeValue", "minValue": 3, "maxValue": 7, "unitCode": "DAY" }
}
},
{
"@type": "OfferShippingDetails",
"shippingRate": { "@type": "MonetaryAmount", "value": 14.99, "currency": "USD" },
"shippingDestination": { "@type": "DefinedRegion", "addressCountry": "GB" },
"deliveryTime": {
"@type": "ShippingDeliveryTime",
"handlingTime": { "@type": "QuantitativeValue", "minValue": 1, "maxValue": 2, "unitCode": "DAY" },
"transitTime": { "@type": "QuantitativeValue", "minValue": 7, "maxValue": 14, "unitCode": "DAY" }
}
}
]
Related guides
Liquid implementation for Dawn themes
Shopify themes generate Product JSON-LD in one of two places: snippets/structured-data.liquid (Dawn and descendants) or inline in sections/main-product.liquid. Find the block containing "@type": "Product" — this is where you will add the shippingDetails array.
Look for the offers block, which typically looks like:
{
"@type": "Offer",
"url": "{{ product.url | prepend: shop.url }}",
"priceCurrency": "{{ shop.currency }}",
"price": "{{ product.selected_or_first_available_variant.price | divided_by: 100.0 | round: 2 }}",
"availability": "https://schema.org/{% if product.available %}InStock{% else %}OutOfStock{% endif %}"
}
Add shippingDetails inside the Offer object, after the availability line. Here is the complete addition using Shopify Liquid for a typical US store with a $50 free shipping threshold. Replace the threshold and transit times with your actual values:
{
"@type": "Offer",
"url": "{{ product.url | prepend: shop.url }}",
"priceCurrency": "{{ shop.currency }}",
"price": "{{ product.selected_or_first_available_variant.price | divided_by: 100.0 | round: 2 }}",
"availability": "https://schema.org/{% if product.available %}InStock{% else %}OutOfStock{% endif %}",
"shippingDetails": [
{
"@type": "OfferShippingDetails",
"shippingRate": {
"@type": "MonetaryAmount",
"value": 5.99,
"currency": "USD"
},
"shippingDestination": {
"@type": "DefinedRegion",
"addressCountry": "US"
},
"deliveryTime": {
"@type": "ShippingDeliveryTime",
"handlingTime": {
"@type": "QuantitativeValue",
"minValue": 0,
"maxValue": 1,
"unitCode": "DAY"
},
"transitTime": {
"@type": "QuantitativeValue",
"minValue": 3,
"maxValue": 7,
"unitCode": "DAY"
},
"cutoffTime": "16:00:00-05:00"
}
},
{
"@type": "OfferShippingDetails",
"shippingRate": {
"@type": "MonetaryAmount",
"value": 0,
"currency": "USD"
},
"shippingDestination": {
"@type": "DefinedRegion",
"addressCountry": "US"
},
"eligibleTransactionVolume": {
"@type": "PriceSpecification",
"minPrice": 50,
"priceCurrency": "USD"
},
"deliveryTime": {
"@type": "ShippingDeliveryTime",
"handlingTime": {
"@type": "QuantitativeValue",
"minValue": 0,
"maxValue": 1,
"unitCode": "DAY"
},
"transitTime": {
"@type": "QuantitativeValue",
"minValue": 3,
"maxValue": 7,
"unitCode": "DAY"
}
}
}
{% unless product.requires_shipping %}
,{
"@type": "OfferShippingDetails",
"doesNotShip": true,
"shippingDestination": {
"@type": "DefinedRegion",
"addressCountry": "US"
}
}
{% endunless %}
]
}
Notes on the Liquid snippet
product.requires_shipping— Shopify's Liquid variable that returnsfalsefor digital products (downloads, gift cards, subscription-only products). The{% unless product.requires_shipping %}block adds adoesNotShip: truesignal for digital items, which correctly excludes them from "need delivery" filtered queries.- Hardcoded vs. dynamic threshold — the
minPrice: 50is intentionally hardcoded here. Shopify does not expose free shipping thresholds as a Liquid variable. If you change your shipping threshold in the Shopify admin, you must also update this value in the snippet. Some merchants store the threshold in a theme setting (settings.free_shipping_threshold) viaconfig/settings_schema.json— that approach allows in-admin updates without touching Liquid code and is recommended for stores that change promotions frequently. - cutoffTime timezone — use the UTC offset for your warehouse location, not a named timezone.
-05:00is US Eastern Standard Time;-08:00is US Pacific Standard Time. If your store observes Daylight Saving Time, you will need to update this offset twice a year, or use a static offset that represents your worst-case handling window. - Multiple Offers vs. one Offer — if your Product JSON-LD currently has one Offer object, this snippet works as-is. If you generate one Offer per variant (more accurate for variant-level pricing but more verbose), add the
shippingDetailsarray to every variant Offer or reference a sharedOfferShippingDetailsnode via@id.
Five mistakes that break shipping schema
shippingDestination entirely
OfferShippingDetails requires shippingDestination — without it, the schema.org validator flags the object as incomplete and Google's rich result testing tool will not recognise the shipping data. A destination-less shipping block is not just incomplete; it is silently ignored by most AI crawlers. Always include at least a country-level DefinedRegion.
"unitText": "days" instead of "unitCode": "DAY"
unitText is a human-readable label — "days", "business days", "d". AI agents and Google's shipping feed processor use unitCode from the UN/CEFACT list for machine interpretation. The correct code for calendar days is "DAY". Using unitText alone will not trigger delivery time display in Google AI Mode shipping badges.
handlingTime + transitTime
ShippingDeliveryTime has two required children: handlingTime and transitTime. Some implementations put a single "total" QuantitativeValue directly on ShippingDeliveryTime — this is not valid. The schema requires the two sub-components. If you are unsure of your handling vs. transit split, err on the side of caution: use handlingTime maxValue: 2 and reduce your transit range accordingly.
shippingDetails (array) with a single object
When a store has multiple shipping options (paid + free threshold, or domestic + international), shippingDetails must be an array of OfferShippingDetails objects, not a single object. Passing a single object works for one option, but adding a second option requires converting to an array. JSON-LD parsers handle both, but the Google Rich Results Test will only recognise the first option if you accidentally nest two objects without an array wrapper.
Unlike availability (which you derive dynamically from Shopify Liquid variables), the free shipping threshold is typically hardcoded. Stores that run seasonal free shipping promotions — "free shipping on everything in December" — often forget to update their JSON-LD. An AI agent that reads "minPrice": 50 from your structured data while your banner says "Free shipping this week, no minimum" will surface the wrong information. Treat shipping schema updates as part of your promotion deployment checklist.
Testing and validation
After adding OfferShippingDetails to your theme, use these tools to verify the implementation:
1. Google Rich Results Test
Visit Google's Rich Results Test (search.google.com/test/rich-results) and enter a product URL. Expand the Product result and look for the offers block — within it, shippingDetails should be listed with its child properties. If you see an error like "Missing field 'shippingDestination'" or "Missing field 'transitTime'", the validator will tell you exactly which required property is absent.
2. schema.org validator
The schema.org validator (validator.schema.org) gives you a lower-level check that is not Google-specific. It validates OfferShippingDetails against the schema.org specification and catches type mismatches (like passing a string where MonetaryAmount is expected) that the Rich Results Test sometimes silently accepts.
3. Google Merchant Center product diagnostics
If your store is linked to Google Merchant Center, the product diagnostics tab shows whether your structured data shipping claims conflict with your Merchant Center shipping settings. Mismatches cause the shipping badge to be suppressed in Google AI Mode even if your JSON-LD is technically valid.
4. CatalogScan
CatalogScan's AI readiness scan checks whether your product pages include shippingDetails as part of its 18-signal audit. The scan flags missing OfferShippingDetails, identifies whether deliveryTime includes both handlingTime and transitTime, and cross-checks the shipping rate against your og:price:amount context. For detailed guidance on the complete structured data testing workflow, see our guide on Shopify structured data audit tools.
After deploying the snippet, allow 3–21 days for AI crawlers to re-index your product pages with the new shipping signals. You can accelerate this by pinging IndexNow for affected product URLs — see our guide on IndexNow for Shopify for the exact workflow. Google's Merchant Center will update faster (usually within 48–72 hours) if you trigger a re-crawl via the product diagnostics interface.
FAQ
OfferShippingDetails JSON-LD by default?Offer containing price, availability, and URL — but no shippingDetails at all. This is true of virtually all Shopify themes. You need to add OfferShippingDetails manually inside the offers block in your theme's structured data snippet, using the Liquid implementation in this guide.OfferShippingDetails and ShippingDeliveryTime?OfferShippingDetails is the parent type: it contains the shipping rate, destination region, optional threshold, and the delivery time block. ShippingDeliveryTime is nested inside deliveryTime and splits the total window into handlingTime (time to dispatch) and transitTime (time in carrier transit). You always need OfferShippingDetails; ShippingDeliveryTime is strongly recommended but technically optional — without it, AI agents cannot answer "can I get it by Thursday?" queries.OfferShippingDetails?eligibleTransactionVolume on a zero-cost OfferShippingDetails block: set shippingRate.value to 0 and eligibleTransactionVolume to a PriceSpecification with minPrice equal to your threshold amount. Include this as a second object in the shippingDetails array alongside your standard paid shipping option, so AI agents can distinguish between the two rates.OfferShippingDetails be at the Product level or the Offer level?shippingDetails is a property of Offer, not Product. It belongs inside your offers block. If you have a single Offer object, place the array directly inside it. If your theme generates one Offer per variant, add shippingDetails to each variant Offer, or define a shared OfferShippingDetails node once with an @id and reference it from each Offer.OfferShippingDetails as part of its AI readiness scan?shippingDetails as a signal gap in its 18-signal audit. It also checks whether ShippingDeliveryTime includes both handlingTime and transitTime, whether a free-shipping threshold is declared via eligibleTransactionVolume, and whether digital products are marked with doesNotShip: true. Run a free scan to see your store's current shipping signal score.Is your store missing shipping schema?
CatalogScan checks all 18 AI readiness signals — including OfferShippingDetails — in under 60 seconds.