Optimization Guide
Shopify Local Pickup and BOPIS Structured Data for AI Shopping Agents
AI shopping agents answer "can I pick this up today in Chicago?" and "buy online pick up in store near me" by reading OfferShippingDetails and LocalBusiness structured data — not your checkout UI. Shopify configures local pickup in the admin, but none of it reaches the product page JSON-LD that AI agents crawl.
OfferShippingDetails with shippingLabel: "local pickup" and DeliveryMethod: OnSitePickup to your product JSON-LD. Add a LocalBusiness entity to your theme.liquid with geo coordinates, address, and OpeningHoursSpecification. For multi-location stores, declare each location as a separate LocalBusiness. Set deliveryTime to reflect realistic pickup readiness (hours, not days) — this is what AI agents use to answer "ready today?" queries.
Why Local Pickup Is Invisible to AI Agents Without Structured Data
Shopify's local pickup feature is a checkout-time decision — the customer selects "Pickup in store" at checkout, sees your store location and hours, and chooses a pickup slot. All of this happens inside the Shopify checkout flow, which AI shopping agents do not crawl. The product page itself — the page that AI agents index and use for recommendations — contains no information about pickup availability.
The result: a store with six physical locations that offers same-day pickup within two hours looks identical to a drop-shipping store with 5-business-day shipping in every AI shopping index. Both have the same InStock availability in their default Shopify JSON-LD. The proximity signal, the "today" signal, and the "no shipping cost" signal are all missing.
AI shopping query types that require local pickup signals
| Query | Required signal | Schema property |
|---|---|---|
| "Available for pickup today in [city]" | OnSitePickup + LocalBusiness geo + deliveryTime ≤ 24h | OfferShippingDetails.availableAtOrFrom |
| "Buy online pick up in store near me" | OnSitePickup + LocalBusiness geo coordinates | LocalBusiness.geo + DeliveryMethod |
| "Same day pickup [product]" | deliveryTime min/max = 0–8 hours | ShippingDeliveryTime.handlingTime |
| "Free pickup [product]" | shippingRate price = 0.00 | OfferShippingDetails.shippingRate |
| "Open today [store type] near me" | LocalBusiness + OpeningHoursSpecification | LocalBusiness.openingHoursSpecification |
Local Pickup JSON-LD Patterns
Minimum viable local pickup — OfferShippingDetails
Add a second OfferShippingDetails entry alongside your standard shipping details. The availableAtOrFrom property links to your LocalBusiness entity via @id:
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Leather Work Tote — Tan",
"offers": {
"@type": "Offer",
"price": "185.00",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock",
"shippingDetails": [
{
"@type": "OfferShippingDetails",
"shippingRate": {
"@type": "MonetaryAmount",
"value": "0.00",
"currency": "USD"
},
"shippingLabel": "local pickup",
"deliveryTime": {
"@type": "ShippingDeliveryTime",
"handlingTime": {
"@type": "QuantitativeValue",
"minValue": 0,
"maxValue": 4,
"unitCode": "HUR"
}
},
"doesNotShip": false,
"deliveryMethod": "http://purl.org/goodrelations/v1#DeliveryModePickUp",
"availableAtOrFrom": {
"@id": "https://yourstore.com/#store-chicago"
}
}
]
}
}
LocalBusiness entity in theme.liquid
Declare your physical store location in layout/theme.liquid. Use an @id that matches the reference in availableAtOrFrom:
{
"@context": "https://schema.org",
"@type": "Store",
"@id": "https://yourstore.com/#store-chicago",
"name": "Your Store — Chicago",
"url": "https://yourstore.com/pages/chicago-store",
"telephone": "+1-312-555-0100",
"address": {
"@type": "PostalAddress",
"streetAddress": "123 W Michigan Ave",
"addressLocality": "Chicago",
"addressRegion": "IL",
"postalCode": "60601",
"addressCountry": "US"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": 41.8819,
"longitude": -87.6278
},
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
"opens": "10:00",
"closes": "19:00"
},
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Saturday"],
"opens": "10:00",
"closes": "18:00"
},
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Sunday"],
"opens": "11:00",
"closes": "17:00"
}
],
"hasMap": "https://maps.google.com/?q=123+W+Michigan+Ave+Chicago+IL"
}
Multi-location store with product-level pickup availability
For stores with multiple locations, declare each as a separate LocalBusiness entity with a unique @id, and reference each from the product's shippingDetails array:
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Leather Work Tote — Tan",
"offers": {
"@type": "Offer",
"price": "185.00",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock",
"shippingDetails": [
{
"@type": "OfferShippingDetails",
"shippingLabel": "Pickup — Chicago (ready in 2h)",
"shippingRate": {
"@type": "MonetaryAmount",
"value": "0.00",
"currency": "USD"
},
"deliveryTime": {
"@type": "ShippingDeliveryTime",
"handlingTime": {
"@type": "QuantitativeValue",
"minValue": 0,
"maxValue": 2,
"unitCode": "HUR"
}
},
"availableAtOrFrom": {
"@id": "https://yourstore.com/#store-chicago"
}
},
{
"@type": "OfferShippingDetails",
"shippingLabel": "Pickup — Brooklyn (ready in 4h)",
"shippingRate": {
"@type": "MonetaryAmount",
"value": "0.00",
"currency": "USD"
},
"deliveryTime": {
"@type": "ShippingDeliveryTime",
"handlingTime": {
"@type": "QuantitativeValue",
"minValue": 0,
"maxValue": 4,
"unitCode": "HUR"
}
},
"availableAtOrFrom": {
"@id": "https://yourstore.com/#store-brooklyn"
}
}
]
}
}
Shopify Liquid Implementation
Shopify POS stores location data in shop.metafields or in a custom metaobject for each location. The cleanest approach is to create a store_locations metaobject list and iterate over it in both your theme.liquid (for LocalBusiness entities) and your product.liquid (for OfferShippingDetails).
store_locations metaobject fields
| Field key | Type | Example | Purpose |
|---|---|---|---|
location_name |
Single line text | Chicago — Michigan Ave |
Display name for LocalBusiness entity |
street_address |
Single line text | 123 W Michigan Ave |
PostalAddress.streetAddress |
city |
Single line text | Chicago |
PostalAddress.addressLocality |
state |
Single line text | IL |
PostalAddress.addressRegion |
postal_code |
Single line text | 60601 |
PostalAddress.postalCode |
latitude |
Number (decimal) | 41.8819 |
GeoCoordinates.latitude |
longitude |
Number (decimal) | -87.6278 |
GeoCoordinates.longitude |
pickup_ready_hours |
Integer | 2 |
deliveryTime.handlingTime.maxValue in hours |
slug |
Single line text | chicago |
Used to construct the @id anchor |
Liquid snippet for OfferShippingDetails (in product.liquid)
{% assign locations = shop.metaobjects.store_locations %}
{% if locations != empty %}
"shippingDetails": [
{% for loc in locations %}
{% unless forloop.first %},{% endunless %}
{
"@type": "OfferShippingDetails",
"shippingLabel": {{ loc.location_name.value | append: ' — ready in ' | append: loc.pickup_ready_hours.value | append: 'h' | json }},
"shippingRate": {
"@type": "MonetaryAmount",
"value": "0.00",
"currency": {{ cart.currency.iso_code | json }}
},
"deliveryTime": {
"@type": "ShippingDeliveryTime",
"handlingTime": {
"@type": "QuantitativeValue",
"minValue": 0,
"maxValue": {{ loc.pickup_ready_hours.value }},
"unitCode": "HUR"
}
},
"availableAtOrFrom": {
"@id": {{ shop.url | append: '/#store-' | append: loc.slug.value | json }}
}
}
{% endfor %}
]
{% endif %}
DeliveryMethod Values Reference
| Schema.org value | Use case | AI agent surfacing context |
|---|---|---|
http://purl.org/goodrelations/v1#DeliveryModePickUp |
Counter/in-store pickup (standard BOPIS) | "Available for in-store pickup today" |
https://schema.org/OnSitePickup |
Pickup at physical business location | "Pick up at store — ready in 2 hours" |
https://schema.org/LockerDelivery |
Self-service locker / pickup box (InPost, Amazon Hub) | "Available at nearby pickup locker" |
https://schema.org/ParcelService |
Third-party carrier parcel delivery | Standard shipping — not local pickup |
Common Mistakes to Avoid
| Mistake | AI agent consequence | Fix |
|---|---|---|
No LocalBusiness entity with geo coordinates |
AI agent can't answer proximity-based "near me" queries | Add LocalBusiness with geo (GeoCoordinates) to theme.liquid |
No OfferShippingDetails for local pickup |
AI agent doesn't know pickup is available; surfaces shipping time only | Add separate OfferShippingDetails entry with OnSitePickup |
deliveryTime set in days rather than hours for same-day pickup |
AI agent surfaces "1-day delivery" instead of "same-day pickup" | Use unitCode: "HUR" (hours) with maxValue: 4 for same-day window |
Missing openingHoursSpecification on LocalBusiness |
AI agent can't answer "open now?" or "open Sunday?" queries | Add full OpeningHoursSpecification array with all days |
| Pickup shippingRate not set to 0.00 | AI agent treats pickup as paid delivery method | Set shippingRate.value: "0.00" — pickup is always free |
Implementation Checklist
- Add
LocalBusiness(orStore) entity tolayout/theme.liquidwith@id, address, andgeocoordinates - Add
openingHoursSpecificationcovering all 7 days (closed days omitted or set with empty hours) - Add
OfferShippingDetailswithOnSitePickupor GoodRelations pickup DeliveryMethod to product JSON-LD - Set
shippingRate.value: "0.00"for free pickup - Set
deliveryTime.handlingTimein hours (unitCode: "HUR") to reflect actual pickup readiness - Wire
availableAtOrFrom.@idto match theLocalBusiness.@idin your layout - Create
store_locationsmetaobject for multi-location stores - For each additional location, create a separate
LocalBusinessentity with unique@id - Validate with Schema.org Validator — confirm LocalBusiness and OfferShippingDetails parse correctly
- Run CatalogScan to confirm local pickup signals appear in AI readiness score
Frequently Asked Questions
How does schema.org model local pickup in structured data?
Use OfferShippingDetails with shippingLabel: "local pickup" and a DeliveryMethod of OnSitePickup. Set deliveryTime with handlingTime in hours to reflect pickup readiness. Pair with a LocalBusiness entity with geo coordinates and opening hours. AI agents process all three signals to answer "available for pickup today near me" queries.
Can AI shopping agents surface local pickup availability for specific locations?
Yes — Google AI Mode and Google Shopping support location-based pickup when your store has a LocalBusiness entity with geo coordinates and your product offers include OnSitePickup OfferShippingDetails. For multi-location stores, declare each location as a separate LocalBusiness with its own geo coordinates. ChatGPT Shopping and Perplexity Shopping also process LocalBusiness geo data for proximity-based recommendations.
Does Shopify automatically add local pickup to product structured data?
No. Shopify's product JSON-LD does not include OfferShippingDetails, DeliveryMethod, or LocalBusiness — even with Shopify POS and local pickup configured in checkout. You must add these signals manually to your theme's product and layout templates.
What is the difference between OnSitePickup and LockerDelivery?
OnSitePickup means staff-assisted counter pickup at your store. LockerDelivery means a self-service locker (InPost, Amazon Hub, etc.) that the customer accesses with a code. For standard BOPIS, use OnSitePickup. Use LockerDelivery only if you use a locker network, not your own store counter.
How do I declare pickup availability hours separately from store open hours?
Use OpeningHoursSpecification on the LocalBusiness entity for regular store hours. If pickup closes earlier (e.g., 30 minutes before store close), note this in the shippingLabel description on OfferShippingDetails. In most cases, pickup hours match store hours — declare them once on LocalBusiness and reference the same entity from availableAtOrFrom.