Issue reference
Your product data does not say what delivery costs
- SCHEMA_OFFER_SHIPPING_MISSING
- Read the details
- —
- Low
What it means
tragent reports this at the “Read the details” stage. The question there is “Can it understand what the product is?” Name, description, images and variants have to be readable as data, not just rendered as a page.
What it does to an agent
Agents get through, but your store reads less clearly to them than it could. It is worth fixing once the higher-severity issues are gone.
A shopper asks for the delivered price, and an agent can only answer it if the shipping cost and delivery time are in the data. Without them it quotes the item price, the shopper sees shipping added at checkout, and the comparison it made was wrong.
You can declare shipping on each Offer, or once for the whole store on your Organization, which Google also accepts. The store-wide form is usually less work.
How to fix it
Add Offer.shippingDetails (rate and delivery time), or one hasShippingService on your Organization.
"shippingDetails": {
"@type": "OfferShippingDetails",
"shippingRate": { "@type": "MonetaryAmount", "value": "4.95", "currency": "USD" },
"shippingDestination": { "@type": "DefinedRegion", "addressCountry": "US" },
"deliveryTime": {
"@type": "ShippingDeliveryTime",
"handlingTime": { "@type": "QuantitativeValue", "minValue": 0, "maxValue": 1, "unitCode": "DAY" },
"transitTime": { "@type": "QuantitativeValue", "minValue": 2, "maxValue": 5, "unitCode": "DAY" }
}
}On your platform
Shopify
The structured_data filter does not include shipping. Add it per offer in a custom block, or add an Organization block to theme.liquid with your shipping service, so it applies to every product.
WooCommerce
WooCommerce does not add shipping to the markup. Add shippingDetails to each offer with the structured data filter, from your flat rate.
// In your child theme's functions.php, or a small plugin.
add_filter( 'woocommerce_structured_data_product', function ( $markup, $product ) {
if ( isset( $markup['offers'][0] ) ) {
$markup['offers'][0]['shippingDetails'] = array(
'@type' => 'OfferShippingDetails',
'shippingRate' => array( '@type' => 'MonetaryAmount', 'value' => '4.95', 'currency' => get_woocommerce_currency() ),
'shippingDestination' => array( '@type' => 'DefinedRegion', 'addressCountry' => 'US' ),
);
}
return $markup;
}, 10, 2 );Headless or custom
Generate the rate from the same table checkout uses, per destination you ship to.
In practice
Model what checkout charges. If shipping is free above a threshold, declare the rate that applies below it and let the agent compare; if you ship to several countries at different rates, declare one shippingDetails per destination. Delivery time is two parts, handling and transit, and agents add them up to answer "will it arrive by Friday", so give both.
Keep the data in step with checkout. A rate in the markup that is lower than checkout charges is the same problem as a wrong price: the agent quotes it and the shopper finds out at the last step. If your rates change often, generate the markup from your shipping configuration rather than writing numbers into a template.
If you offer several speeds, such as standard and express, declare each as its own shippingDetails with its rate and delivery time. Agents answering "cheapest" and agents answering "fastest" pick different ones, and a store that declares only standard shipping loses the shopper who needed it tomorrow, even though you could have delivered.
Check the fix
- Check the JSON-LD for shippingDetails on an offer, or hasShippingService on the Organization.
- Run the scan; the shipping finding should be gone.
Run the check that reported SCHEMA_OFFER_SHIPPING_MISSING again after the change. The scan needs no account; an issue found during an AI shopping test needs that test run again.
npx @tragent/cli scan https://your-store.comCommon mistakes
- Declaring free shipping in the data when it applies only over a threshold.
- A rate for one country with no destination, which reads as worldwide.