← Back to KPIs

Curbside Pickup Rate

Also known as: Curbside Pickup Share, Curbside Order Share, Curbside Pickup Mix, Curbside Adoption Rate, Curbside Share of BOPIS Orders

OperationskpiRetail: Omnichannel (BOPIS - Buy Online Pick Up In Store)Retail

Curbside Pickup Rate is the share of all buy-online-pickup-in-store (BOPIS) orders placed in a period for which the customer chose curbside handoff, with the attendant bringing the order to the vehicle, instead of collecting it at the in-store pickup counter or locker. It is a fulfilment-mix measure used to staff curbside bays and set their capacity, and it is distinct from Pickup Completion Rate, which measures whether pickup orders are actually collected, not how the customer chose to collect them.

Business Use Case

1️⃣ Store Operations reviews the rate by store and hour band each Monday, and when a store's curbside share holds above 40% for three consecutive weeks it adds a dedicated curbside runner to peak pickup windows and requests an extra marked bay from Facilities. 2️⃣ The Omnichannel Product team tests the default handoff option on the checkout fulfilment selector; if a variant shifts the curbside rate by more than 5 points without lowering Pickup Completion Rate, it rolls the variant out and updates the pickup-slot capacity model. 3️⃣ Customer Service correlates weekly curbside rate with curbside wait-time complaints per store, and when complaints per 1,000 curbside orders rise while the rate climbs it escalates to the regional manager to recalibrate the promised handoff time shown in the ready-for-pickup notification. 4️⃣ Merchandising limits curbside eligibility for bulky or age-restricted categories in stores where the rate exceeds bay capacity, and re-checks the category-level rate the following month to confirm the shift to counter pickup.

Business Criticality

Medium

Journey / Navigation Area

Checkout

Owning Function

Store Operations

Scope

Event

Formula

Curbside Pickup Rate (%) = (COUNT(DISTINCT transaction_id) of BOPIS orders placed in the period with fulfillment_method = 'curbside' / COUNT(DISTINCT transaction_id) of all BOPIS orders placed in the period, where fulfillment_method IN ('pickup_in_store', 'curbside')) × 100

Calculation Notes

1️⃣ Denominator is the full BOPIS base: every order placed in the period with at least one line fulfilled by store pickup (fulfillment_method 'pickup_in_store' or 'curbside' on the purchase event), counted once per transaction_id. Ship-to-home, ship-to-store and same-day delivery orders are excluded from both sides, so the rate describes the pickup mix and never the share of all online orders (that is BOPIS Order Share). 2️⃣ Curbside classification uses the fulfillment method on the order header in the commerce platform, joined to the GA4 purchase event on transaction_id; where the two disagree the platform value wins, because customers can switch from counter to curbside in the order-status page or the app after checkout, and that final selection before arrival is what the store fulfils. 3️⃣ Order grain, not line or unit grain: a split order carrying both pickup and ship-to-home lines counts once in the denominator, and counts as curbside only when its pickup lines are designated curbside; a BOPIS order split across two pickup stores counts once, attributed to the store holding the higher-value pickup group. 4️⃣ Cancelled and uncollected orders stay in both numerator and denominator, because the KPI measures the customer's handoff choice at placement; whether the order was collected is measured by Pickup Completion Rate and must not be folded into this number. Test orders, fraud-cancelled orders voided before release to the store, and employee exchange orders are excluded. 5️⃣ Timing: attribute each order to the reporting period of its placement timestamp in the store's local time zone, not the pickup or check-in timestamp, so the numerator and denominator share one cohort. Orders placed near midnight whose pickup falls in the next period stay in the placement period. 6️⃣ Store eligibility: the headline rate uses all BOPIS orders, including stores without curbside bays, as the denominator. When reviewing a single store or comparing stores, break the rate down by the curbside-enabled store flag, because a chain-level drop can simply reflect new stores without curbside rather than lower customer adoption. 7️⃣ Curbside reclassified at arrival (the customer taps 'I'm here' from the car for a counter order and the store accepts the handoff) is not counted as curbside here; that handoff mode is reported from the Order Pickup event and belongs in pickup operations reporting.

Technical Details

Measure TypePercentage
Aggregation WindowDaily, Weekly, Monthly, Quarterly

Events

PlatformEvent
Google Analytics 4purchase
Adobepurchase

Data Mappings

W3 Data Layer
{
  "digitalData": {
    "event": [
      {
        "eventInfo": {
          "eventName": "purchase"
        }
      }
    ],
    "transaction": {
      "transactionID": "ORD-2049183",
      "total": {
        "basePrice": 39.53,
        "taxRate": 3.44,
        "shipping": 0,
        "transactionTotal": 42.97,
        "currency": "USD"
      },
      "profile": {
        "userID": "u_7788213",
        "customerType": "returning"
      },
      "attributes": {
        "fulfillmentMethod": "curbside",
        "checkoutType": "bopis",
        "shippingTier": "pickup",
        "affiliation": "Store 482",
        "paymentType": "credit_card"
      },
      "item": [
        {
          "productID": "SKU-10293",
          "productInfo": {
            "productName": "Organic Whole Milk 1 Gal",
            "category": "Grocery"
          },
          "price": 4.99,
          "quantity": 3
        },
        {
          "productID": "SKU-55210",
          "productInfo": {
            "productName": "Rotisserie Chicken",
            "category": "Deli"
          },
          "price": 8.99,
          "quantity": 3
        }
      ]
    }
  }
}
GA4 Data Layer
{
  "event": "purchase",
  "ecommerce": {
    "transaction_id": "ORD-2049183",
    "affiliation": "Store 482",
    "value": 42.97,
    "currency": "USD",
    "tax": 3.44,
    "shipping": 0,
    "coupon": "",
    "payment_type": "credit_card",
    "checkout_type": "bopis",
    "shipping_tier": "pickup",
    "items": [
      {
        "item_id": "SKU-10293",
        "item_name": "Organic Whole Milk 1 Gal",
        "item_category": "Grocery",
        "price": 4.99,
        "quantity": 3
      },
      {
        "item_id": "SKU-55210",
        "item_name": "Rotisserie Chicken",
        "item_category": "Deli",
        "price": 8.99,
        "quantity": 3
      }
    ]
  },
  "user_id": "u_7788213",
  "customer_type": "returning",
  "fulfillment_method": "curbside",
  "traffic_type": "app"
}
Adobe Client Data Layer
{
  "event": "purchase",
  "commerce": {
    "purchases": {
      "value": 1
    },
    "order": {
      "orderId": "ORD-2049183",
      "priceTotal": 42.97,
      "currencyCode": "USD",
      "purchaseTax": 3.44,
      "affiliation": "Store 482",
      "payments": [
        {
          "paymentType": "credit_card"
        }
      ]
    }
  },
  "fulfillment": {
    "fulfillmentMethod": "curbside",
    "checkoutType": "bopis",
    "shippingTier": "pickup"
  },
  "product": [
    {
      "productID": "SKU-10293",
      "productName": "Organic Whole Milk 1 Gal",
      "category": "Grocery",
      "price": 4.99,
      "quantity": 3
    },
    {
      "productID": "SKU-55210",
      "productName": "Rotisserie Chicken",
      "category": "Deli",
      "price": 8.99,
      "quantity": 3
    }
  ],
  "user": {
    "userID": "u_7788213",
    "customerType": "returning"
  }
}
XDM Mapping
{
  "eventType": "commerce.purchases",
  "commerce": {
    "purchases": {
      "value": 1
    },
    "order": {
      "purchaseID": "ORD-2049183",
      "priceTotal": 42.97,
      "currencyCode": "USD"
    }
  },
  "productListItems": [
    {
      "SKU": "SKU-10293",
      "name": "Organic Whole Milk 1 Gal",
      "category": "Grocery",
      "priceTotal": 4.99,
      "quantity": 3
    },
    {
      "SKU": "SKU-55210",
      "name": "Rotisserie Chicken",
      "category": "Deli",
      "priceTotal": 8.99,
      "quantity": 3
    }
  ],
  "_orgkpis": {
    "fulfillmentMethod": "curbside",
    "checkoutType": "bopis",
    "shippingTier": "pickup",
    "affiliation": "Store 482",
    "paymentType": "credit_card"
  }
}

SQL Queries

Google Analytics 4 (BigQuery)
-- ==========================================
-- 1. GOOGLE BIGQUERY SQL
-- ==========================================
DECLARE start_date DATE DEFAULT DATE_SUB(CURRENT_DATE(), INTERVAL 90 DAY);
DECLARE end_date DATE DEFAULT DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY);

-- One row per order: the first purchase hit per transaction_id with its checkout fulfillment method (suffix widened one day each side for store time zones)
WITH ga4_orders AS (
  SELECT
    ecommerce.transaction_id AS transaction_id,
    ARRAY_AGG(STRUCT(
      event_timestamp,
      (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'fulfillment_method') AS fulfillment_method
    ) ORDER BY event_timestamp LIMIT 1)[OFFSET(0)] AS first_hit
  FROM `your-project.analytics_123456789.events_*`
  WHERE _TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(start_date, INTERVAL 1 DAY))
                          AND FORMAT_DATE('%Y%m%d', DATE_ADD(end_date, INTERVAL 1 DAY))
    AND event_name = 'purchase'
    AND ecommerce.transaction_id IS NOT NULL
  GROUP BY transaction_id
),
-- Platform header wins: last pre-arrival pickup selection of the pickup lines (arrival reclassification not included), placement date in store local time; test, fraud-voided-before-release and employee exchange orders excluded
classified AS (
  SELECT
    g.transaction_id,
    IF(o.order_id IS NOT NULL, o.selected_pickup_method, g.first_hit.fulfillment_method) AS fulfillment_method,
    IF(o.order_id IS NOT NULL, DATE(o.placed_at, o.store_time_zone), DATE(TIMESTAMP_MICROS(g.first_hit.event_timestamp))) AS placed_date
  FROM ga4_orders AS g
  LEFT JOIN `your-project.commerce.orders` AS o
    ON o.order_id = g.transaction_id
  WHERE COALESCE(o.is_test_order, FALSE) = FALSE
    AND COALESCE(o.is_fraud_voided_before_release, FALSE) = FALSE
    AND COALESCE(o.is_employee_exchange, FALSE) = FALSE
)
-- Curbside orders over all BOPIS orders placed in the period, one count per order; cancelled and uncollected orders stay on both sides
SELECT
  ROUND(SAFE_DIVIDE(
    COUNT(DISTINCT IF(fulfillment_method = 'curbside', transaction_id, NULL)),
    COUNT(DISTINCT transaction_id)
  ) * 100, 2) AS curbside_pickup_rate_pct
FROM classified
WHERE fulfillment_method IN ('pickup_in_store', 'curbside')
  AND placed_date BETWEEN start_date AND end_date;
Adobe Experience Platform (AEP Query Service)
-- ==========================================
-- 2. ADOBE AEP QUERY SERVICE SQL
-- ==========================================
-- Reporting window: last 90 complete days
WITH params AS (
  SELECT CAST(CURRENT_DATE - INTERVAL '90' DAY AS DATE) AS start_date,
         CAST(CURRENT_DATE - INTERVAL '1' DAY AS DATE) AS end_date
),
-- One row per order: the first purchase hit per purchaseID with its checkout fulfillment method (window widened one day each side for store time zones)
aep_orders AS (
  SELECT transaction_id, fulfillment_method, event_ts
  FROM (
    SELECT
      e.commerce.order.purchaseID AS transaction_id,
      e._orgkpis.fulfillmentMethod AS fulfillment_method,
      e.timestamp AS event_ts,
      ROW_NUMBER() OVER (PARTITION BY e.commerce.order.purchaseID ORDER BY e.timestamp) AS rn
    FROM aep_web_dataset AS e
    CROSS JOIN params AS p
    WHERE e.eventType = 'commerce.purchases'
      AND e.commerce.order.purchaseID IS NOT NULL
      AND e.timestamp >= p.start_date - INTERVAL '1' DAY
      AND e.timestamp < p.end_date + INTERVAL '2' DAY
  ) AS hits
  WHERE rn = 1
),
-- Platform header wins: last pre-arrival pickup selection of the pickup lines (arrival reclassification not included), placement date in store local time; test, fraud-voided-before-release and employee exchange orders excluded
classified AS (
  SELECT
    a.transaction_id,
    CASE WHEN o.order_id IS NOT NULL THEN o.selected_pickup_method ELSE a.fulfillment_method END AS fulfillment_method,
    CASE WHEN o.order_id IS NOT NULL THEN TO_DATE(FROM_UTC_TIMESTAMP(o.placed_at, o.store_time_zone))
         ELSE TO_DATE(a.event_ts) END AS placed_date
  FROM aep_orders AS a
  LEFT JOIN commerce_orders_dataset AS o
    ON o.order_id = a.transaction_id
  WHERE COALESCE(o.is_test_order, FALSE) = FALSE
    AND COALESCE(o.is_fraud_voided_before_release, FALSE) = FALSE
    AND COALESCE(o.is_employee_exchange, FALSE) = FALSE
)
-- Curbside orders over all BOPIS orders placed in the period, one count per order; cancelled and uncollected orders stay on both sides
SELECT
  ROUND(
    100.0 * COUNT(DISTINCT CASE WHEN c.fulfillment_method = 'curbside' THEN c.transaction_id END)
    / NULLIF(COUNT(DISTINCT c.transaction_id), 0), 2) AS curbside_pickup_rate_pct
FROM classified AS c
CROSS JOIN params AS p
WHERE c.fulfillment_method IN ('pickup_in_store', 'curbside')
  AND c.placed_date BETWEEN p.start_date AND p.end_date;

Source Data

Digital Analytics (GA4) + Commerce Platform Orders (Shopify / Salesforce Commerce Cloud / Adobe Commerce)

Dependencies

Prerequisite: List the dependencies required for this KPI

Events:

purchaseorder-pickup

Metrics:

bopis-orders

Dimensions:

fulfillment-methodpickup-window

Report Attributes

Store Region Curbside-Enabled Store Flag Fulfillment Method Pickup Window Order Channel (Web / App) Device Category Item Category Customer Loyalty Tier Day of Week

Recommended Reporting Views

Views
Omnichannel Fulfilment PerformanceStore Operations Weekly ReviewBOPIS & Curbside Capacity Planning

Segment Eligibility

true

Governance

Data SensitivityInternal

Governance

Created bydevyendarm
Created on9/21/2026
Last modified bydevyendarm
Last modified on9/21/2026
Version1.0.0
Validation stateqa passed
StatusPUBLISHED

GitHub

View related Pull Requestcatalog/kpis/curbside-pickup-rate.yml

Community Discussion