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.

TL;DR Add 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 keyTypeExamplePurpose
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

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.

Related Resources