Pickup Window
Also known as: Pickup Slot, Pickup Time Slot, Collection Window
The pickup time slot promised to, or selected by, the customer on a buy-online-pick-up-in-store order — expressed as a governed label such as "Within 2 hours", "Same day", "Next day" or "Scheduled slot" — captured on the unified add_shipping_info and purchase events. It is the customer-facing collection promise made at checkout and the primary breakdown for Pickup Completion Rate, not-collected volume and BOPIS-driven cancellation and refund analysis.
Business Use Case
1️⃣ The Omnichannel Merchandising manager reviews Pickup Completion Rate by window each week and, where the "Within 2 hours" band falls materially below the same-day band at a store cluster, withdraws the two-hour promise from those stores in the checkout configuration until ready-to-collect performance recovers.,2️⃣ The Store Operations lead is alerted when not-collected orders concentrate in the "Next day" and "Scheduled slot" bands, and responds by tightening the reminder and hold-period cadence for those windows before the automatic cancellation and refund is triggered.,3️⃣ The Digital Product manager for checkout uses the window mix on add_shipping_info versus purchase to identify slots that are selected but abandoned, and re-orders or re-labels the pickup options on the delivery step to lead with the windows that convert.,4️⃣ The Customer Service planning team forecasts contact volume by window and staffs the store and contact-centre lines ahead of peak trading, using the share of orders promised into same-day and two-hour bands as the driver for "where is my order" and collection-support demand.
Business Criticality
High
Journey / Navigation Area
Checkout
Owning Function
Merchandising
Scope
Event
Calculation Notes
1️⃣ The value is always the promised slot LABEL or BAND as a string ("Within 2 hours", "Same day", "Next day", "Scheduled slot"), never a raw timestamp: an absolute ready-by or collect-by time belongs in a datetime field, and the gap between the promised and the actual collection time is a separate metric, not a value of this dimension.,2️⃣ The value is first set when the shopper confirms a pickup option at checkout (add_shipping_info) and is sealed on purchase; the value present on the purchase event is the promise of record for conversion, checkout-funnel and cohort reporting, so downstream restatement of that value is not permitted.,3️⃣ Only the checkout-time promise is captured: if the customer later reschedules the window in order management, that change is not written back to this dimension, and the purchase-event value remains the promised window of record for the order.,4️⃣ An expired, missed or auto-cancelled window never overwrites the dimension with "Expired" or "Missed": the collection outcome lives in the order or pickup status, and this dimension continues to carry the window that was promised, so that not-collected volume can be attributed back to the slot that generated it.,5️⃣ Excluded: ship-to-home, ship-from-store and carrier-delivery orders (the dimension is populated only on events whose Fulfillment Method is a pickup variant; all other events carry no value and are excluded from the pickup base population rather than reported as missing), plus store staffing, picking-capacity, labour-hour and slot-inventory measures — this dimension is the customer-facing promise only and must not be reused as a capacity or workload attribute.,6️⃣ Values are normalised to the governed list at collection time: free-form or locale-specific slot strings from the commerce platform are mapped to the standard bands, slot windows are evaluated in the store's local time rather than the shopper's, cardinality is capped by banding scheduled slots rather than emitting every calendar slot, and any unmapped or missing selection is recorded as "Unspecified" instead of null.,7️⃣ Timing and stitching: the checkout selection is attributed within the standard 30-minute session-inactivity window, and the window is carried once per event at order (cart) level.
Technical Details
Events
| Platform | Event |
|---|---|
| Google Analytics 4 | add_shipping_info |
| Adobe | commerce.checkoutShippingInfoAdded |
Data Mappings
W3 Data Layer
{
"digitalData": {
"pageInstanceID": "checkout-delivery-prod",
"page": {
"pageInfo": {
"pageID": "checkout-delivery-options",
"pageName": "Checkout: Delivery & Pickup Options"
},
"category": {
"primaryCategory": "Checkout",
"subCategory1": "Fulfillment Selection"
}
},
"cart": {
"cartID": "CART-8842190",
"price": {
"cartTotal": 149.5,
"currency": "USD"
},
"attributes": {
"checkoutStep": "delivery_options",
"fulfillmentMethod": "bopis",
"pickupWindow": "Within 2 hours",
"pickupWindowSource": "customer_selected",
"pickupWindowStart": "2026-09-17T14:00:00-05:00",
"pickupWindowEnd": "2026-09-17T16:00:00-05:00",
"pickupStoreID": "STR-0421",
"pickupStoreTimezone": "America/Chicago"
}
},
"event": [
{
"eventInfo": {
"eventName": "add_shipping_info",
"eventAction": "Shipping Info Added",
"type": "checkout",
"timeStamp": "2026-09-17T14:03:12.000-05:00"
},
"attributes": {
"fulfillmentMethod": "bopis",
"pickupWindow": "Within 2 hours",
"pickupStoreID": "STR-0421"
}
}
]
}
}GA4 Data Layer
{
"event": "add_shipping_info",
"ecommerce": {
"currency": "USD",
"value": 149.5,
"shipping_tier": "Store Pickup",
"items": [
{
"item_id": "SKU-77341",
"item_name": "Trail Runner Jacket",
"item_brand": "Northbound",
"item_category": "Apparel",
"price": 74.75,
"quantity": 2,
"affiliation": "Store 0421"
}
]
},
"fulfillment_method": "bopis",
"pickup_window": "Within 2 hours",
"pickup_window_source": "customer_selected",
"pickup_window_start": "2026-09-17T14:00:00-05:00",
"pickup_window_end": "2026-09-17T16:00:00-05:00",
"pickup_store_id": "STR-0421",
"pickup_store_timezone": "America/Chicago",
"checkout_step": "delivery_options"
}Adobe Client Data Layer
{
"event": "commerce.checkoutShippingInfoAdded",
"eventInfo": {
"trigger": "Checkout: Shipping Info Added",
"path": "commerce.fulfillment",
"timestamp": "2026-09-17T14:03:12.000-05:00"
},
"commerce": {
"checkout": {
"step": "delivery_options",
"cartId": "CART-8842190",
"currencyCode": "USD",
"priceTotal": 149.5
},
"fulfillment": {
"fulfillmentMethod": "bopis",
"shippingMethod": "Store Pickup",
"pickupWindow": "Within 2 hours",
"pickupWindowSource": "customer_selected",
"pickupWindowStart": "2026-09-17T14:00:00-05:00",
"pickupWindowEnd": "2026-09-17T16:00:00-05:00",
"pickupStoreId": "STR-0421",
"pickupStoreTimezone": "America/Chicago"
},
"productListItems": [
{
"SKU": "SKU-77341",
"name": "Trail Runner Jacket",
"quantity": 2,
"priceTotal": 149.5
}
]
}
}XDM Mapping
{
"eventType": "commerce.checkouts",
"timestamp": "2026-09-17T19:03:12.000Z",
"commerce": {
"checkouts": {
"value": 1
},
"order": {
"currencyCode": "USD",
"priceTotal": 149.5
},
"shipping": {
"shippingMethod": "store-pickup"
}
},
"productListItems": [
{
"SKU": "SKU-77341",
"name": "Trail Runner Jacket",
"quantity": 2,
"priceTotal": 149.5
}
],
"_experience": {
"analytics": {
"customDimensions": {
"eVars": {
"eVar27": "Within 2 hours",
"eVar28": "bopis",
"eVar29": "STR-0421"
}
}
}
},
"_openkpis": {
"fulfillment": {
"fulfillmentMethod": "bopis",
"pickupWindow": "Within 2 hours",
"pickupWindowSource": "customer_selected",
"pickupWindowStart": "2026-09-17T14:00:00-05:00",
"pickupWindowEnd": "2026-09-17T16:00:00-05:00",
"pickupStoreId": "STR-0421",
"pickupStoreTimezone": "America/Chicago"
}
}
}SQL Queries
Google Analytics 4 (BigQuery)
-- ==========================================
-- 1. GOOGLE BIGQUERY SQL
-- ==========================================
-- Dimension: Pickup Window (pickup-window) | data_type string | scope Event
-- Returns the promised pickup slot LABEL and its distribution across the two
-- carrier events, add_shipping_info (selection) and purchase (sealed promise).
-- Base population = pickup-eligible events only: ship-to-home and carrier
-- delivery orders have no window by design and are never counted as a gap.
-- The label is read as a string; promised-vs-actual collection duration is a
-- separate metric and is deliberately not computed here.
WITH event_grain AS (
-- event_params are pivoted once per event with UNNEST + MAX(IF(...)),
-- so each event contributes exactly one row (no fan-out join).
SELECT
PARSE_DATE('%Y%m%d', evt.event_date) AS event_day,
evt.event_name AS event_name,
evt.user_pseudo_id AS user_pseudo_id,
evt.event_timestamp AS event_timestamp,
evt.event_bundle_sequence_id AS event_bundle_sequence_id,
evt.device.category AS device_category,
MAX(IF(prm.key = 'ga_session_id', prm.value.int_value, NULL)) AS ga_session_id,
MAX(IF(prm.key = 'transaction_id', prm.value.string_value, NULL)) AS transaction_id,
LOWER(TRIM(MAX(IF(prm.key = 'fulfillment_method', prm.value.string_value, NULL)))) AS fulfillment_method,
TRIM(MAX(IF(prm.key = 'pickup_window', prm.value.string_value, NULL))) AS pickup_window_raw,
MAX(IF(prm.key = 'pickup_window_source', prm.value.string_value, NULL)) AS pickup_window_source,
MAX(IF(prm.key = 'pickup_store_id', prm.value.string_value, NULL)) AS pickup_store_id,
MAX(IF(prm.key = 'value',
COALESCE(prm.value.double_value, CAST(prm.value.int_value AS FLOAT64)),
NULL)) AS event_value
FROM `your-project.analytics_123456789.events_*` AS evt,
UNNEST(evt.event_params) AS prm
WHERE _TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 90 DAY))
AND FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY))
AND evt.event_name IN ('add_shipping_info', 'purchase')
GROUP BY event_day, event_name, user_pseudo_id, event_timestamp, event_bundle_sequence_id, device_category
),
pickup_base AS (
-- Scope guard: pickup-eligible fulfilment methods are the full base population
-- for every share and coverage figure below.
SELECT
event_day,
event_name,
user_pseudo_id,
transaction_id,
device_category,
pickup_store_id,
event_value,
CONCAT(user_pseudo_id, '.', CAST(ga_session_id AS STRING)) AS session_key,
COALESCE(pickup_window_source, 'unspecified') AS pickup_window_source,
COALESCE(NULLIF(pickup_window_raw, ''), 'Unspecified') AS pickup_window,
CASE
WHEN TRIM(COALESCE(pickup_window_raw, '')) = '' THEN 'Missing selection'
WHEN pickup_window_raw = 'Unspecified' THEN 'Missing selection'
WHEN pickup_window_raw IN ('Within 2 hours', 'Same day', 'Next day', 'Scheduled slot') THEN 'Governed value'
ELSE 'Unmapped value'
END AS value_governance
FROM event_grain
-- Governed fulfillment_method values as emitted by the data layer (e.g. 'bopis').
WHERE fulfillment_method IN (
'bopis', 'curbside', 'click_and_collect', 'locker_pickup', 'ship_to_store'
)
)
SELECT
event_name,
pickup_window,
value_governance,
pickup_window_source,
COUNT(*) AS events,
COUNT(DISTINCT session_key) AS sessions,
COUNT(DISTINCT user_pseudo_id) AS users,
COUNT(DISTINCT IF(event_name = 'purchase', transaction_id, NULL)) AS orders,
COUNT(DISTINCT pickup_store_id) AS pickup_stores,
ROUND(SUM(IF(event_name = 'purchase', event_value, 0)), 2) AS purchase_value,
-- Denominator is every pickup-eligible event of that carrier event, not the
-- missing or unmapped subset.
ROUND(100 * COUNT(*) / SUM(COUNT(*)) OVER (PARTITION BY event_name), 2) AS pct_of_pickup_eligible_events,
ROUND(100 * SUM(COUNTIF(value_governance = 'Governed value')) OVER (PARTITION BY event_name)
/ SUM(COUNT(*)) OVER (PARTITION BY event_name), 2) AS governed_value_coverage_pct
FROM pickup_base
GROUP BY event_name, pickup_window, value_governance, pickup_window_source
ORDER BY event_name, events DESC;Adobe Experience Platform (AEP Query Service)
-- ==========================================
-- 2. ADOBE AEP QUERY SERVICE SQL
-- ==========================================
-- Dimension: Pickup Window (pickup-window) | data_type string | scope Event
-- ExperienceEvent dataset, reading the XDM paths declared in xdm_mapping:
-- _openkpis.fulfillment.pickupWindow / .fulfillmentMethod / .pickupWindowSource / .pickupStoreId
-- mirrored in _experience.analytics.customDimensions.eVars.eVar27 / eVar28 / eVar29
-- Carrier events: commerce.checkouts (selection) and commerce.purchases (sealed promise).
-- Base population = pickup-eligible events only; ship-to-home has no window and
-- is excluded rather than reported as missing. The value is the slot label string.
WITH sessionised AS (
SELECT
to_date(timestamp) AS event_day,
eventType AS event_type,
endUserIDs._experience.mcid.id AS visitor_id,
commerce.order.purchaseID AS order_id,
commerce.order.priceTotal AS order_value,
lower(trim(coalesce(
_openkpis.fulfillment.fulfillmentMethod,
_experience.analytics.customDimensions.eVars.eVar28,
commerce.shipping.shippingMethod))) AS fulfillment_method,
trim(coalesce(
_openkpis.fulfillment.pickupWindow,
_experience.analytics.customDimensions.eVars.eVar27)) AS pickup_window_raw,
_openkpis.fulfillment.pickupWindowSource AS pickup_window_source,
coalesce(
_openkpis.fulfillment.pickupStoreId,
_experience.analytics.customDimensions.eVars.eVar29) AS pickup_store_id,
SESS_TIMEOUT(timestamp, 60 * 30) OVER (
PARTITION BY endUserIDs._experience.mcid.id
ORDER BY timestamp ASC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS session
FROM experience_events
WHERE timestamp >= date_sub(current_date, 90)
AND timestamp < current_date
AND eventType IN ('commerce.checkouts', 'commerce.purchases')
),
pickup_base AS (
SELECT
event_day,
event_type,
visitor_id,
order_id,
order_value,
pickup_store_id,
concat(visitor_id, '-', cast(session.num AS string)) AS session_key,
coalesce(pickup_window_source, 'unspecified') AS pickup_window_source,
coalesce(nullif(pickup_window_raw, ''), 'Unspecified') AS pickup_window,
CASE
WHEN trim(coalesce(pickup_window_raw, '')) = '' THEN 'Missing selection'
WHEN pickup_window_raw = 'Unspecified' THEN 'Missing selection'
WHEN pickup_window_raw IN ('Within 2 hours', 'Same day', 'Next day', 'Scheduled slot') THEN 'Governed value'
ELSE 'Unmapped value'
END AS value_governance
FROM sessionised
-- Same governed fulfillment_method values as the BigQuery query; 'store-pickup' is the
-- extra match for the commerce.shipping.shippingMethod fallback.
WHERE fulfillment_method IN (
'bopis', 'curbside', 'click_and_collect', 'locker_pickup', 'ship_to_store',
'store-pickup'
)
)
SELECT
event_type,
pickup_window,
value_governance,
pickup_window_source,
count(*) AS events,
count(DISTINCT session_key) AS sessions,
count(DISTINCT visitor_id) AS visitors,
count(DISTINCT CASE WHEN event_type = 'commerce.purchases' THEN order_id END) AS orders,
count(DISTINCT pickup_store_id) AS pickup_stores,
round(sum(CASE WHEN event_type = 'commerce.purchases' THEN order_value ELSE 0 END), 2) AS purchase_value,
-- Denominator is every pickup-eligible event of that event type.
round(100.0 * count(*) / sum(count(*)) OVER (PARTITION BY event_type), 2) AS pct_of_pickup_eligible_events,
round(100.0 * sum(count(CASE WHEN value_governance = 'Governed value' THEN 1 END)) OVER (PARTITION BY event_type)
/ sum(count(*)) OVER (PARTITION BY event_type), 2) AS governed_value_coverage_pct
FROM pickup_base
GROUP BY event_type, pickup_window, value_governance, pickup_window_source
ORDER BY event_type, events DESC;Source Data
Digital + Order Management (Salesforce Commerce Cloud), stitched to in-store collection confirmations
Dependencies
Prerequisite: List the dependencies required for this Dimension
Report Attributes
Store ID Fulfillment Method Region / Store Cluster Order Value Band Device Category Day of Week Hour of Day New vs Returning Customer
Segment Eligibility
true
Governance
Derived Dimensions
Governance
GitHub
catalog/dimensions/pickup-window.yml