Return Reason
Also known as: Reason for Return, Return Reason Code, RMA Reason, Returns Reason
Return Reason is the standardised code a customer selects for each returned line item when starting a return after delivery, such as Size Too Small, Size Too Large, Defective or Damaged, Not as Described, Wrong Item Sent, Unwanted or Changed Mind, and Arrived Too Late. It explains why post-purchase revenue is reversed, so Merchandising, Customer Service and Fulfilment can separate fit, quality, content-accuracy and delivery failures from buyer's remorse; it covers returns only, not pre-shipment cancellations, which are tracked in Order Cancellation Reason.
Business Use Case
1️⃣ The Merchandising team reviews return share by reason every week for each SKU and size. When SIZE_TOO_SMALL or SIZE_TOO_LARGE exceeds the category baseline for two weeks in a row, they update size guidance and fit notes on the product page and ask the supplier to review the grading.,2️⃣ Customer Service leads monitor DEFECTIVE and damaged-on-arrival reasons each day by carrier and fulfilment location. When one lane or packaging type spikes, they open a carrier claim, change packaging instructions for the affected items and trigger proactive replacement messages for open orders on that route.,3️⃣ The E-commerce content team gets a monthly list of the products with the highest NOT_AS_DESCRIBED and COLOR shares. They reshoot product images, correct colour names and rewrite specifications for those products, then compare the reason share in the 30 days before and after the change.,4️⃣ Finance and Revenue Management split refund liability by reason when setting the monthly returns reserve. When UNWANTED returns rise faster than product-fault returns, they propose restocking fees or shorter return windows for the affected categories and model the net revenue impact.
Business Criticality
High
Journey / Navigation Area
Monetization
Owning Function
Customer Service
Scope
Event
Calculation Notes
1️⃣ Grain and capture point: set one value per returned line item (order_id + line_item_id + return_id) captured when the customer submits the return request in the self-service returns portal, order-history page or app, and stored with the return on the backend RMA record. Because the refund event reduces reported revenue, it is sent server-side (GA4 Measurement Protocol / AEP server-side Edge API) by the OMS once the return is confirmed and the refund amount is approved, never from the browser or at submission, so the payload carries no page context; abandoned or unapproved requests send no refund event. That refund event carries the stored reason: each returned item carries line_item_id, return_reason and return_reason_raw, and the event value is the approved refund amount (the sum of the approved returned lines). Record the customer's selection as submitted, not the reason the warehouse assigns at inspection. Store the inspection outcome (restockable, damaged in transit, customer-damaged) in a separate disposition attribute so the stated reason is never overwritten.,2️⃣ Controlled vocabulary: map platform codes to one governed list. Shopify ReturnReason uses SIZE_TOO_SMALL, SIZE_TOO_LARGE, COLOR, STYLE, DEFECTIVE, NOT_AS_DESCRIBED, WRONG_ITEM, UNWANTED, OTHER and UNKNOWN. Map Salesforce Order Management ReturnOrderLineItem.ReasonForReturn picklist values and Adobe Commerce RMA reason options (e.g. Wrong Size, Wrong Color, Out of Service, Other) to the same list. Keep the raw platform code alongside the governed value for traceability. For executive reporting, roll the governed codes up in the reporting layer into five groups (Product Fault, Fit & Size, Not as Described, Delivery Issue, Changed Mind); this roll-up is a lookup on the governed code, not a separately collected value.,3️⃣ Scope boundary with Order Cancellation Reason: this dimension covers only returns against orders that have shipped or been delivered. A request raised before shipment, including one handled by the returns portal but stopped before the carrier handover, is a cancellation and belongs in order-cancellation-reason. Never merge the two lists: cancellations reflect checkout, payment and stock failures, while returns reflect product and delivery experience after the customer has the item.,4️⃣ Multi-select and sequencing: if the portal allows more than one reason per line, keep the first reason selected as the primary value and store the others in a secondary array. Line-level counts must stay additive. For multi-unit lines, give each unit the reason on its own return line and do not copy one reason across a quantity greater than 1 unless the customer confirmed it for all units.,5️⃣ Exclusions: leave out test orders, internal and employee accounts, and fraud-flagged returns (keep them in a separate audit view). Also leave out exchanges that the platform books as a new order with no refund, unless you report them under a separate return_type = exchange flag. Carrier-initiated return-to-sender parcels (refused, undeliverable) get the system value RETURN_TO_SENDER, never a customer reason, because the customer made no selection.,6️⃣ Free text and nulls: never use the optional comment field as a dimension value. It can contain names, addresses or order details, so keep it in a restricted table for text analysis. A return created by an agent in the customer-service console with no reason selected gets UNKNOWN, not OTHER. Report UNKNOWN share as a data-quality control with a target below 5% of returned lines.,7️⃣ Timing and attribution window: date the value on the refund event (return approval) timestamp in the store's reporting time zone, keeping the return-request timestamp on the RMA record for request-to-approval analysis, and link it back to the original purchase by transaction_id so it can be broken down by acquisition channel and campaign. Standard analysis windows are 30, 60 and 90 days after delivery, in line with common return-policy periods. Returns arriving after the policy window stay in the data but carry an out_of_policy flag.
Technical Details
Events
| Platform | Event |
|---|---|
| Google Analytics 4 | refund |
| Adobe | refund |
Data Mappings
W3 Data Layer
{
"event": {
"eventInfo": {
"eventName": "refund",
"eventSource": "server"
}
},
"transaction": {
"transactionID": "T-100482",
"attributes": {
"returnID": "RMA-55821",
"returnType": "refund",
"returnMethod": "mail",
"outOfPolicy": false
},
"total": {
"currency": "USD",
"refundTotal": 59.98
},
"item": [
{
"productInfo": {
"productID": "SKU-20931",
"productName": "Linen Shirt",
"sku": "SKU-20931-M-BLU"
},
"category": {
"primaryCategory": "Apparel/Shirts"
},
"quantity": 1,
"price": {
"basePrice": 29.99,
"currency": "USD"
},
"attributes": {
"lineItemID": "LI-1",
"variant": "M / Blue",
"returnReason": "SIZE_TOO_SMALL",
"returnReasonRaw": "SIZE_TOO_SMALL",
"returnReasonSecondary": []
}
},
{
"productInfo": {
"productID": "SKU-18804",
"productName": "Ceramic Mug",
"sku": "SKU-18804-WHT"
},
"category": {
"primaryCategory": "Home/Kitchen"
},
"quantity": 1,
"price": {
"basePrice": 29.99,
"currency": "USD"
},
"attributes": {
"lineItemID": "LI-2",
"variant": "White",
"returnReason": "DEFECTIVE",
"returnReasonRaw": "DEFECTIVE",
"returnReasonSecondary": []
}
}
]
}
}GA4 Data Layer
{
"event": "refund",
"return_id": "RMA-55821",
"return_type": "refund",
"return_method": "mail",
"out_of_policy": false,
"ecommerce": {
"transaction_id": "T-100482",
"currency": "USD",
"value": 59.98,
"items": [
{
"item_id": "SKU-20931",
"item_name": "Linen Shirt",
"item_category": "Apparel",
"item_category2": "Shirts",
"item_variant": "M / Blue",
"price": 29.99,
"quantity": 1,
"line_item_id": "LI-1",
"return_reason": "SIZE_TOO_SMALL",
"return_reason_raw": "SIZE_TOO_SMALL"
},
{
"item_id": "SKU-18804",
"item_name": "Ceramic Mug",
"item_category": "Home",
"item_category2": "Kitchen",
"item_variant": "White",
"price": 29.99,
"quantity": 1,
"line_item_id": "LI-2",
"return_reason": "DEFECTIVE",
"return_reason_raw": "DEFECTIVE"
}
]
}
}Adobe Client Data Layer
{
"event": "refund",
"eventInfo": {
"eventName": "refund",
"linkName": "refund",
"linkType": "o"
},
"transaction": {
"transactionID": "T-100482",
"returnID": "RMA-55821",
"returnType": "refund",
"returnMethod": "mail",
"outOfPolicy": false,
"currency": "USD",
"refundTotal": 59.98,
"item": [
{
"productID": "SKU-20931",
"productName": "Linen Shirt",
"category": "Apparel/Shirts",
"variant": "M / Blue",
"quantity": 1,
"price": 29.99,
"lineItemID": "LI-1",
"returnReason": "SIZE_TOO_SMALL",
"returnReasonRaw": "SIZE_TOO_SMALL"
},
{
"productID": "SKU-18804",
"productName": "Ceramic Mug",
"category": "Home/Kitchen",
"variant": "White",
"quantity": 1,
"price": 29.99,
"lineItemID": "LI-2",
"returnReason": "DEFECTIVE",
"returnReasonRaw": "DEFECTIVE"
}
]
}
}XDM Mapping
{
"eventType": "web.webinteraction.linkClicks",
"web": {
"webInteraction": {
"name": "refund",
"type": "other",
"linkClicks": {
"value": 1
}
}
},
"commerce": {
"order": {
"purchaseID": "T-100482",
"currencyCode": "USD",
"priceTotal": 59.98
}
},
"productListItems": [
{
"SKU": "SKU-20931",
"name": "Linen Shirt",
"quantity": 1,
"priceTotal": 29.99,
"_experience": {
"analytics": {
"customDimensions": {
"eVars": {
"eVar40": "SIZE_TOO_SMALL",
"eVar43": "LI-1",
"eVar44": "SIZE_TOO_SMALL",
"eVar47": "Apparel"
}
}
}
}
},
{
"SKU": "SKU-18804",
"name": "Ceramic Mug",
"quantity": 1,
"priceTotal": 29.99,
"_experience": {
"analytics": {
"customDimensions": {
"eVars": {
"eVar40": "DEFECTIVE",
"eVar43": "LI-2",
"eVar44": "DEFECTIVE",
"eVar47": "Home"
}
}
}
}
}
],
"_experience": {
"analytics": {
"customDimensions": {
"eVars": {
"eVar41": "RMA-55821",
"eVar42": "refund",
"eVar45": "mail",
"eVar46": "false"
}
}
}
}
}SQL Queries
Google Analytics 4 (BigQuery)
-- ==========================================
-- 1. GOOGLE BIGQUERY SQL
-- ==========================================
-- Return Reason: returned line items by governed reason code, one row per day x reason x return type x item category.
-- Grain: transaction_id + return_id + line_item_id (item_id fallback). The free-text return comment is never selected.
-- Share denominator: all returned line items of the same day (full base population), not a single reason.
WITH refund_events AS (
SELECT
DATE(TIMESTAMP_MICROS(event_timestamp), 'America/New_York') AS report_date,
event_timestamp,
user_pseudo_id,
COALESCE(
ecommerce.transaction_id,
(SELECT MAX(IF(ep.key = 'transaction_id', ep.value.string_value, NULL)) FROM UNNEST(event_params) AS ep)
) AS transaction_id,
(SELECT MAX(IF(ep.key = 'return_id', ep.value.string_value, NULL)) FROM UNNEST(event_params) AS ep) AS return_id,
(SELECT MAX(IF(ep.key = 'return_type', ep.value.string_value, NULL)) FROM UNNEST(event_params) AS ep) AS return_type,
(SELECT MAX(IF(ep.key = 'return_method', ep.value.string_value, NULL)) FROM UNNEST(event_params) AS ep) AS return_method,
items
FROM `your-project.analytics_123456789.events_*`
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 event_name = 'refund'
),
return_lines AS (
SELECT
r.report_date,
r.event_timestamp,
r.transaction_id,
r.return_id,
COALESCE(LOWER(r.return_type), 'refund') AS return_type,
r.return_method,
it.item_id,
it.item_name,
it.item_category,
it.item_variant,
COALESCE(it.quantity, 1) AS quantity,
COALESCE(it.price, 0) * COALESCE(it.quantity, 1) AS refunded_value,
(SELECT MAX(IF(ip.key = 'line_item_id', COALESCE(ip.value.string_value, CAST(ip.value.int_value AS STRING)), NULL))
FROM UNNEST(it.item_params) AS ip) AS line_item_id,
(SELECT MAX(IF(ip.key = 'return_reason', ip.value.string_value, NULL))
FROM UNNEST(it.item_params) AS ip) AS return_reason_value,
(SELECT MAX(IF(ip.key = 'return_reason_raw', ip.value.string_value, NULL))
FROM UNNEST(it.item_params) AS ip) AS return_reason_raw
FROM refund_events AS r,
UNNEST(r.items) AS it
WHERE r.transaction_id IS NOT NULL
),
deduplicated_lines AS (
SELECT
*,
COALESCE(NULLIF(UPPER(TRIM(return_reason_value)), ''), 'UNKNOWN') AS return_reason,
CONCAT(transaction_id, '|', COALESCE(return_id, 'NO_RETURN_ID'), '|', COALESCE(line_item_id, item_id)) AS return_line_key
FROM return_lines
WHERE TRUE
QUALIFY ROW_NUMBER() OVER (
PARTITION BY transaction_id, COALESCE(return_id, 'NO_RETURN_ID'), COALESCE(line_item_id, item_id)
ORDER BY event_timestamp
) = 1
),
reason_daily AS (
SELECT
report_date,
return_reason,
return_type,
item_category,
COUNT(DISTINCT return_line_key) AS returned_lines,
SUM(quantity) AS returned_units,
ROUND(SUM(refunded_value), 2) AS refunded_value,
COUNT(DISTINCT transaction_id) AS orders_with_returns,
COUNT(DISTINCT return_id) AS return_requests
FROM deduplicated_lines
GROUP BY report_date, return_reason, return_type, item_category
)
SELECT
report_date,
return_reason,
return_type,
item_category,
returned_lines,
returned_units,
refunded_value,
orders_with_returns,
return_requests,
SAFE_DIVIDE(returned_lines, SUM(returned_lines) OVER (PARTITION BY report_date)) AS share_of_returned_lines,
SAFE_DIVIDE(
SUM(IF(return_reason = 'UNKNOWN', returned_lines, 0)) OVER (PARTITION BY report_date),
SUM(returned_lines) OVER (PARTITION BY report_date)
) AS unknown_reason_share_of_day
FROM reason_daily
ORDER BY report_date DESC, returned_lines DESC;Adobe Experience Platform (AEP Query Service)
-- ==========================================
-- 2. ADOBE AEP QUERY SERVICE SQL
-- ==========================================
-- Return Reason: returned line items by governed reason code (productListItems eVar40), per day x reason x return type x item category.
-- Return id = event-level eVar41, return type = event-level eVar42; line item id = productListItems eVar43, raw reason code = productListItems eVar44, item category = productListItems eVar47 (matches GA4 item_category).
-- Grain: transaction_id + return_id + line item id (SKU fallback), matching the GA4 line grain. The free-text return comment is never selected.
-- Share denominator: all returned line items of the same day (full base population).
WITH refund_events AS (
SELECT
_id AS event_id,
timestamp AS event_ts,
TO_DATE(FROM_UTC_TIMESTAMP(timestamp, 'America/New_York')) AS report_date,
commerce.order.purchaseID AS transaction_id,
_experience.analytics.customDimensions.eVars.eVar41 AS return_id,
LOWER(COALESCE(_experience.analytics.customDimensions.eVars.eVar42, 'refund')) AS return_type,
productListItems
FROM your_aep_web_event_dataset
WHERE eventType = 'web.webinteraction.linkClicks'
AND web.webInteraction.name = 'refund'
AND timestamp >= DATE_SUB(CURRENT_DATE(), 90)
AND timestamp < CURRENT_DATE()
AND commerce.order.purchaseID IS NOT NULL
),
return_lines AS (
SELECT
r.event_id,
r.event_ts,
r.report_date,
r.transaction_id,
r.return_id,
r.return_type,
pli.SKU AS sku,
COALESCE(NULLIF(TRIM(pli._experience.analytics.customDimensions.eVars.eVar43), ''), pli.SKU) AS line_key_id,
pli.name AS product_name,
COALESCE(NULLIF(TRIM(pli._experience.analytics.customDimensions.eVars.eVar47), ''), '(not set)') AS item_category,
COALESCE(pli.quantity, 1) AS quantity,
COALESCE(pli.priceTotal, 0) AS refunded_value,
COALESCE(NULLIF(UPPER(TRIM(pli._experience.analytics.customDimensions.eVars.eVar40)), ''), 'UNKNOWN') AS return_reason,
pli._experience.analytics.customDimensions.eVars.eVar44 AS return_reason_raw
FROM refund_events r
LATERAL VIEW EXPLODE(r.productListItems) pli_table AS pli
),
deduplicated_lines AS (
SELECT *
FROM (
SELECT
rl.*,
CONCAT(transaction_id, '|', COALESCE(return_id, 'NO_RETURN_ID'), '|', line_key_id) AS return_line_key,
ROW_NUMBER() OVER (
PARTITION BY transaction_id, COALESCE(return_id, 'NO_RETURN_ID'), line_key_id
ORDER BY event_ts
) AS rn
FROM return_lines rl
) ranked
WHERE rn = 1
),
reason_daily AS (
SELECT
report_date,
return_reason,
return_type,
item_category,
COUNT(DISTINCT return_line_key) AS returned_lines,
SUM(quantity) AS returned_units,
ROUND(SUM(refunded_value), 2) AS refunded_value,
COUNT(DISTINCT transaction_id) AS orders_with_returns,
COUNT(DISTINCT return_id) AS return_requests
FROM deduplicated_lines
GROUP BY report_date, return_reason, return_type, item_category
)
SELECT
report_date,
return_reason,
return_type,
item_category,
returned_lines,
returned_units,
refunded_value,
orders_with_returns,
return_requests,
CAST(returned_lines AS DOUBLE)
/ NULLIF(SUM(returned_lines) OVER (PARTITION BY report_date), 0) AS share_of_returned_lines,
CAST(SUM(CASE WHEN return_reason = 'UNKNOWN' THEN returned_lines ELSE 0 END) OVER (PARTITION BY report_date) AS DOUBLE)
/ NULLIF(SUM(returned_lines) OVER (PARTITION BY report_date), 0) AS unknown_reason_share_of_day
FROM reason_daily
ORDER BY report_date DESC, returned_lines DESC;Source Data
Digital + OMS Returns (Shopify Returns / Salesforce Order Management / Adobe Commerce RMA)
Dependencies
Prerequisite: List the dependencies required for this Dimension
Events:
Dimensions:
Report Attributes
Item Category Product Name Item Variant (Size / Colour) Carrier Fulfilment Location Return Method (Mail / In-Store / Drop-Off) Days from Delivery to Return Request New vs Returning Customer Original Acquisition Channel
Segment Eligibility
true
Governance
Derived Dimensions
Governance
GitHub
catalog/dimensions/return-reason.yml