HomeBlog › Shopify local pickup and BOPIS structured data

Shopify local pickup and BOPIS structured data: how Buy Online Pick Up In Store signals reach AI shopping agents

2026-06-10  ·  ~14 min read  ·  BOPIS schema local pickup OfferShippingDetails LocalBusiness AI shopping

Shopify configures local pickup inside the checkout flow — a UI that AI shopping agents never crawl. A Shopify store with six physical locations looks identical to a drop-shipper in every AI shopping index. The gap is not a Shopify bug: it’s a missing JSON-LD layer you own.

0%
of Shopify stores emit OfferShippingDetails in default product JSON-LD, even with POS + local pickup configured
~1 in 4
AI shopping queries now include a proximity or urgency modifier: "near me," "today," "available now"
$0
shipping cost for pickup — the strongest conversion signal for cost-sensitive AI shopping comparisons

In this guide

  1. Why BOPIS is invisible to AI agents by default
  2. The two-entity architecture: Product + LocalBusiness
  3. OfferShippingDetails: wiring pickup into the product Offer
  4. LocalBusiness entity: geo, address, and opening hours
  5. Multi-location BOPIS strategy
  6. The "ready today" signal: deliveryTime in hours
  7. Pickup hours vs. store hours: OpeningHoursSpecification nuance
  8. Curbside pickup and locker delivery variants
  9. Shopify POS: why admin config doesn't equal structured data
  10. 5 BOPIS schema mistakes
  11. FAQ

Why BOPIS is invisible to AI agents by default

Local pickup in Shopify works like this: a customer reaches the checkout, selects "Pickup in store," sees your available locations with hours and estimated ready-times, and confirms their order. This entire experience lives inside Shopify's checkout flow — a set of pages that AI shopping agents (ChatGPT Shopping, Perplexity Commerce, Google AI Mode) never crawl. They crawl product pages, not checkout pages.

The product page JSON-LD that AI agents read contains a Product type, an Offer with price and availability, and perhaps a Brand and AggregateRating. Shopify's default Liquid template outputs none of the following — even for a store with Shopify POS active and five locations configured:

The result: a multi-location brick-and-mortar store offering same-day free pickup within two hours emits the same structured data as a warehouse in New Jersey with a 5-day shipping timeline. Both show "availability": "https://schema.org/InStock". Nothing else. An AI agent answering "is there somewhere I can pick this up today in Austin?" has no signal to distinguish them.

The opportunity cost: local pickup is a conversion advantage AI agents could amplify for free. Zero shipping cost. Same-day availability. Physical proximity. Each of these signals directly answers urgency-modifying queries that represent a growing share of AI shopping traffic. None of them reach AI agents without structured data you add manually.

The two-entity architecture: Product + LocalBusiness

BOPIS structured data requires two entity types working together. Neither is sufficient alone:

OfferShippingDetails (inside the Product Offer)

Declares that a pickup delivery method exists for this product, what it costs (zero), how long until the order is ready, and where it can be picked up. The "where" is a reference (via availableAtOrFrom) to a LocalBusiness entity defined elsewhere.

LocalBusiness (in theme.liquid, sitewide)

Declares your physical store location(s): name, address, geo coordinates, phone, and opening hours. Does not belong in the product JSON-LD block — it lives in your layout file so it's present on every page, and products reference it by @id.

The connection between them is the availableAtOrFrom property on OfferShippingDetails, which takes an @id reference that matches the @id on the corresponding LocalBusiness entity. AI agents process both together: the product signals what pickup costs and how long it takes; the location entity answers where and during what hours.

OfferShippingDetails: wiring pickup into the product Offer

Add a second entry to your product Offer's shippingDetails array alongside your standard shipping option. The minimum viable pickup block:

{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "{{ product.title | json }}",
  "offers": {
    "@type": "Offer",
    "price": "{{ product.price | money_without_currency | json }}",
    "priceCurrency": "{{ cart.currency.iso_code }}",
    "availability": "https://schema.org/InStock",
    "shippingDetails": [
      {
        "@type": "OfferShippingDetails",
        "shippingDestination": {
          "@type": "DefinedRegion",
          "addressCountry": "US"
        },
        "shippingRate": {
          "@type": "MonetaryAmount",
          "value": "5.95",
          "currency": "USD"
        },
        "deliveryTime": {
          "@type": "ShippingDeliveryTime",
          "handlingTime": {
            "@type": "QuantitativeValue",
            "minValue": 1, "maxValue": 2, "unitCode": "DAY"
          },
          "transitTime": {
            "@type": "QuantitativeValue",
            "minValue": 3, "maxValue": 5, "unitCode": "DAY"
          }
        }
      },
      {
        "@type": "OfferShippingDetails",
        "shippingLabel": "local pickup",
        "shippingRate": {
          "@type": "MonetaryAmount",
          "value": "0.00",
          "currency": "USD"
        },
        "deliveryTime": {
          "@type": "ShippingDeliveryTime",
          "handlingTime": {
            "@type": "QuantitativeValue",
            "minValue": 0, "maxValue": 2, "unitCode": "HUR"
          }
        },
        "deliveryMethod": "http://purl.org/goodrelations/v1#DeliveryModePickUp",
        "availableAtOrFrom": {
          "@id": "https://yourstore.com/#store-main"
        }
      }
    ]
  }
}

The key properties in the pickup block:

Property Value Why it matters
shippingLabel "local pickup" Human-readable label; AI agents surface this in recommendation copy
shippingRate.value "0.00" Free pickup is a conversion signal; agents filter on "free shipping" + "free pickup"
deliveryTime.handlingTime minValue: 0, maxValue: 2, unitCode: "HUR" Hours-based unit triggers "same day" and "today" query matching — see the ready-today signal
deliveryMethod GoodRelations DeliveryModePickUp The canonical DeliveryMethod value for in-store pickup; required for Google AI Mode proximity filtering
availableAtOrFrom.@id URL matching LocalBusiness @id Links the pickup option to a specific location entity with address and hours

LocalBusiness entity: geo, address, and opening hours

The LocalBusiness (or Store subtype) entity belongs in layout/theme.liquid, not in your product JSON-LD. Put it in a <script type="application/ld+json"> block in the <head> so it's present on every page:

{
  "@context": "https://schema.org",
  "@type": "Store",
  "@id": "https://yourstore.com/#store-main",
  "name": "Your Store — Downtown",
  "url": "https://yourstore.com/pages/store-locations",
  "telephone": "+1-512-555-0100",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "215 S Congress Ave",
    "addressLocality": "Austin",
    "addressRegion": "TX",
    "postalCode": "78704",
    "addressCountry": "US"
  },
  "geo": {
    "@type": "GeoCoordinates",
    "latitude": 30.2542,
    "longitude": -97.7473
  },
  "openingHoursSpecification": [
    {
      "@type": "OpeningHoursSpecification",
      "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
      "opens": "10:00",
      "closes": "20: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=215+S+Congress+Ave+Austin+TX+78704"
}

Three properties that directly move AI agent behavior:

Multi-location BOPIS strategy

For stores with more than one physical location, each location needs its own LocalBusiness entity with a unique @id. Product offers reference each location separately in the shippingDetails array. This is how AI agents determine which of your locations has a product available for same-day pickup near a specific user.

{
  "@type": "Offer",
  "availability": "https://schema.org/InStock",
  "shippingDetails": [
    {
      "@type": "OfferShippingDetails",
      "shippingLabel": "Pickup — Austin Downtown (ready 2h)",
      "shippingRate": { "@type": "MonetaryAmount", "value": "0.00", "currency": "USD" },
      "deliveryTime": {
        "@type": "ShippingDeliveryTime",
        "handlingTime": { "@type": "QuantitativeValue", "minValue": 0, "maxValue": 2, "unitCode": "HUR" }
      },
      "deliveryMethod": "http://purl.org/goodrelations/v1#DeliveryModePickUp",
      "availableAtOrFrom": { "@id": "https://yourstore.com/#store-austin-downtown" }
    },
    {
      "@type": "OfferShippingDetails",
      "shippingLabel": "Pickup — Austin North (ready 4h)",
      "shippingRate": { "@type": "MonetaryAmount", "value": "0.00", "currency": "USD" },
      "deliveryTime": {
        "@type": "ShippingDeliveryTime",
        "handlingTime": { "@type": "QuantitativeValue", "minValue": 0, "maxValue": 4, "unitCode": "HUR" }
      },
      "deliveryMethod": "http://purl.org/goodrelations/v1#DeliveryModePickUp",
      "availableAtOrFrom": { "@id": "https://yourstore.com/#store-austin-north" }
    }
  ]
}

Each LocalBusiness entity is declared in theme.liquid with its own @id, address, and GeoCoordinates. AI agents can then resolve the closest pickup location to the user's query and surface a specific "available for pickup in 2 hours at 215 S Congress Ave" recommendation rather than a generic "available for pickup" claim.

A practical strategy for multi-location stores: if pickup readiness varies by location (your downtown flagship can pack and ready orders in 2 hours; your suburban outlet takes 4), reflect that in the handlingTime per location. Agents use this to rank same-day options when a user's query includes a time constraint.

The "ready today" signal: deliveryTime in hours

This is the single detail that separates stores winning "available today" queries from those missing them entirely. Shopify stores that output shippingDetails at all typically copy from an e-commerce template that sets transitTime in days. Days are correct for standard shipping; they are wrong for in-store pickup.

AI agents parse handlingTime to determine pickup urgency:

handlingTime value unitCode Query it can answer
minValue: 0, maxValue: 2 HUR "Available today," "same day pickup," "can I pick this up in 2 hours"
minValue: 0, maxValue: 8 HUR "Available today," "pickup today" — upper bound within business hours
minValue: 1, maxValue: 1 DAY "Next day pickup" — will NOT match "today" or "same day" queries
minValue: 2, maxValue: 5 DAY No pickup urgency signal — indistinguishable from standard shipping in AI agent priority

Use UN/CEFACT unit codes: HUR for hours, DAY for days. Omitting unitCode or using a non-standard string leaves the agent to guess, and guesses on pickup timing default to conservative (slower) estimates that exclude you from urgency queries.

Pair handlingTime hours with openingHoursSpecification on the LocalBusiness entity. Google AI Mode specifically applies opening hours to validate that a "ready in 2 hours" claim is plausible given the current time. A store that claims 2-hour pickup but has no declared hours (or hours that show the store is currently closed) loses the same-day qualification.

Pickup hours vs. store hours: OpeningHoursSpecification nuance

Most stores use the same hours for pickup as for in-store shopping, and declaring one openingHoursSpecification on the LocalBusiness entity covers both. But two edge cases create a mismatch worth handling explicitly:

Pickup window closes before the store closes. If your store is open until 9 PM but stops accepting new BOPIS orders at 7 PM (because staff starts end-of-day close procedures), your handlingTime maxValue should reflect the earlier cutoff. Either adjust your handlingTime conservatively (e.g., maxValue: 6 HUR so it doesn't promise same-day near the window edge) or add a separate openingHoursSpecification specifically for the pickup service:

{
  "@type": "OpeningHoursSpecification",
  "name": "BOPIS order cutoff",
  "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],
  "opens": "10:00",
  "closes": "19:00"
}

Seasonal hours changes. Use validFrom and validThrough on individual OpeningHoursSpecification entries to declare temporary hours (holiday extensions, summer schedule changes) without replacing your year-round spec. An agent reading your JSON-LD during the holiday season will see the extended hours and qualify your store for those additional "available tonight" windows.

Curbside pickup and locker delivery variants

BOPIS and curbside pickup use the same DeliveryMethod value in structured data (DeliveryModePickUp). If you want to differentiate curbside from counter pickup, add an additionalProperty to the OfferShippingDetails block:

{
  "@type": "OfferShippingDetails",
  "shippingLabel": "curbside pickup",
  "deliveryMethod": "http://purl.org/goodrelations/v1#DeliveryModePickUp",
  "additionalProperty": {
    "@type": "PropertyValue",
    "propertyID": "fulfillmentType",
    "value": "curbside"
  },
  "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-main" }
}

For self-service locker pickup (third-party networks like Amazon Hub, InPost, or your own secure pickup box), use "deliveryMethod": "https://schema.org/LockerDelivery" and declare the locker location as a separate LockerDelivery-typed LocalBusiness entity. Locker delivery is not the same as in-store pickup: the locker may be at a different address than your store, may have 24/7 access (declare via openingHoursSpecification with all 7 days, 00:00–23:59), and carries no geographic connection to your main store location.

Shopify POS: why admin config doesn't equal structured data

This is the source of the most widespread confusion around Shopify local pickup. Shopify POS is a comprehensive point-of-sale system that handles in-person inventory, staff permissions, location-level stock, and in-store order creation. When you activate local pickup in Shopify Shipping settings, you're configuring checkout-time pickup options. When you add a POS location, you're configuring inventory routing and in-store terminals.

None of this configuration produces structured data. Shopify's theme engine (Dawn, Sense, Refresh, and every third-party theme) does not read POS location data and inject it into product page JSON-LD. The Shopify Liquid variables for location data (shop.enabled_payment_types, location.*, etc.) are not exposed in the context of the sections/main-product.liquid template that powers the product page JSON-LD block.

The practical consequence: your Shopify POS location list is the source of truth for the addresses, hours, and geo coordinates you need — but you have to transcribe that data manually into your LocalBusiness entities in theme.liquid. There is no automatic sync. If you add a new POS location or change store hours, update your LocalBusiness JSON-LD at the same time.

Tip: If you have a Shopify store location page (e.g., /pages/store-locations), add a LocalBusiness entity to that page's template in addition to the one in theme.liquid. Having both the sitewide and page-level entity increases the signal density that AI agents read when crawling your location page.

5 BOPIS schema mistakes

#1
Declaring the LocalBusiness entity in product JSON-LD instead of theme.liquid
Embedding the LocalBusiness entity inside each product's JSON-LD block duplicates a large chunk of data on every product page, bloats your page weight, and makes it hard to update hours without touching every product template. The LocalBusiness entity is sitewide infrastructure — it belongs in layout/theme.liquid once, referenced by @id from product-level OfferShippingDetails.
#2
Using deliveryTime in days (not hours) for pickup
Setting handlingTime to minValue: 1, maxValue: 1, unitCode: "DAY" is technically valid for pickup, but it will not qualify your store for "same day" or "available today" AI agent queries. Pickup readiness is measured in hours. Use unitCode: "HUR" with a maxValue that reflects your actual order-ready time (2, 4, or 8 hours for most stores). If your store genuinely needs 24 hours to pack BOPIS orders, use 1 DAY — but most stores are not in that category.
#3
Omitting GeoCoordinates from the LocalBusiness entity
An address is human-readable. GeoCoordinates are machine-readable. AI agents use latitude and longitude — not street addresses — to compute proximity to a user's location. A LocalBusiness with a perfectly formatted PostalAddress but no geo block is invisible in "near me" query resolution. Get your store's latitude and longitude from Google Maps (right-click → "What's here?") and add them to the geo block.
#4
Setting shippingRate to null or omitting it for pickup
When shippingRate is omitted from the pickup OfferShippingDetails, AI agents cannot verify that pickup is free. This blocks your store from "free pickup near me" queries and from price comparisons where the agent factors in total cost including shipping. Always declare shippingRate: {"@type": "MonetaryAmount", "value": "0.00", "currency": "USD"} on every pickup OfferShippingDetails, even if the value feels obvious.
#5
Not updating structured data when store hours change
AI agents cache structured data for weeks. If you change your store hours for summer, for the holidays, or because you added Sunday trading — and don't update the openingHoursSpecification in your theme.liquid LocalBusiness entity — agents will continue citing your old hours. Google AI Mode specifically shows "open/closed" status in BOPIS recommendations based on structured data. A mismatch between your actual hours and your declared hours erodes recommendation confidence for your location.

FAQ

Does Shopify automatically add local pickup to product JSON-LD?

No. Shopify's native product JSON-LD does not include OfferShippingDetails, DeliveryMethod, or LocalBusiness entities — even when you have Shopify POS and local pickup fully configured in your checkout. The pickup option appears in the Shopify checkout UI, but none of this data surfaces in the product page structured data that AI shopping agents crawl. You must add local pickup signals manually: an OfferShippingDetails block in your product JSON-LD and a LocalBusiness entity in layout/theme.liquid.

What schema.org type should I use for the local pickup delivery method?

For standard BOPIS (staff-assisted counter pickup), use deliveryMethod: "http://purl.org/goodrelations/v1#DeliveryModePickUp". For curbside pickup, use the same value and add an additionalProperty with propertyID: "fulfillmentType" and value: "curbside". For self-service locker pickup, use "deliveryMethod": "https://schema.org/LockerDelivery". For in-store-only products not available to order online, set availability: "https://schema.org/InStoreOnly" on the Offer rather than an OfferShippingDetails block.

How do AI shopping agents use LocalBusiness geo coordinates for "near me" queries?

AI shopping agents like Google AI Mode and ChatGPT Shopping process GeoCoordinates on your LocalBusiness entity to match store locations to user proximity signals. When a user asks "coffee grinder available for pickup in Chicago," the agent cross-references your LocalBusiness geo coordinates against the query's location entity and filters for stores within a reasonable radius. Without GeoCoordinates on a LocalBusiness entity linked from your product OfferShippingDetails, the agent has no proximity signal — your store looks identical to a pure e-commerce store with no physical locations.

How do I handle multi-location Shopify stores in structured data?

Declare each physical location as a separate LocalBusiness entity in theme.liquid, each with a unique @id (e.g., https://yourstore.com/#store-chicago, https://yourstore.com/#store-austin). In your product JSON-LD, include a separate OfferShippingDetails entry for each location, each with availableAtOrFrom referencing the location's @id and a deliveryTime reflecting that location's pickup readiness. This lets AI agents match the correct pickup location to the user's proximity query and surface which specific location has same-day availability.

What deliveryTime value should I use to win "same day pickup" queries?

For same-day pickup qualification, set handlingTime minValue to 0 and maxValue to your realistic order-ready-by time in hours (e.g., 2, 4, or 8), using unitCode: "HUR". Avoid days as the unit — an agent parsing handlingTime of 1 DAY won't qualify your store for "available today" queries the way 8 HUR would. If your store typically has orders ready within 2 hours during business hours, set minValue: 0, maxValue: 2, unitCode: "HUR". This is the most direct signal an AI agent reads to determine pickup urgency.

Is your store visible for local pickup queries?

Run a free scan to check your structured data signals — including OfferShippingDetails and LocalBusiness coverage.

Scan your store free Full JSON-LD reference