Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions docs/specification/return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<!--
Copyright 2026 UCP Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

# Return Extension

## Overview

The Return Extension allows businesses to communicate the conditions, methods, timelines, and costs associated with returning physical items directly to the platform and the buyer, mirroring real-world commerce requirements.

By exposing the return policy natively in the UCP schema, AI agents and platforms can intelligently answer user queries like *"Can I return this in-store?"* or *"How many days do I have to return this?"* without forcing the user to leave the platform to hunt for a policy on the merchant's website.

This extension adds a `return_policies` field to Checkout containing:

* `return_policies[]` — conditions governed by the merchant for specific items.
* `window_type` — the category of return window (finite, lifetime, final sale, etc.)
* `return_days` — the number of days in the window.
* `methods[]` — permitted physical methods (in-store, by-mail, etc.)
* `fee` — the cost structure for that specific method.

**Mental model:**

* `return_policies[0]` Standard Apparel
* `line_item_ids` 👕👖
* `window_type` = `finite_window` 🗓️ 30 Days
* `methods[0]` In-Store 🏬
* `fee` = `free` ✅
* `methods[1]` By Mail 📦
* `fee` = `fixed_fee` $5.00 💸
* `return_policies[1]` Final Sale
* `line_item_ids` ⌚
* `window_type` = `final_sale` 🚫
* `exchanges_allowed` = `false`

Comment thread
venkatesh-ucp marked this conversation as resolved.
## Schema

Return policies apply to physical items in a checkout session. Items not governed by a specific policy (e.g., digital services) may be omitted or covered by a default policy.

### Properties

{{ extension_fields('return', 'return_policies') }}

### Entities

#### Return Policy

{{ schema_fields('types/return_policy', 'return') }}

#### Return Method

{{ schema_fields('types/return_method', 'return') }}

#### Return Fee

{{ schema_fields('types/return_fee', 'return') }}

## Rendering

Return policies are designed for proactive disclosures by the merchant. Platforms use these fields to provide transparency about the logistical requirements of a purchase before completion.

### Human-Readable Fields

| Location | Field | Required | Purpose |
| ---------------------- | -------------- | -------- | --------------------------------------------------- |
| `return_policy` | `return_days` | No | Quantitative window for the return. |
| `return_method.fee` | `display_text` | No | Context for the fee (e.g., "Restocking Fee"). |
| `return_method.fee` | `amount` | No | Price in minor units for fixed fees. |

### Business Responsibilities
Comment thread
venkatesh-ucp marked this conversation as resolved.
Comment thread
venkatesh-ucp marked this conversation as resolved.

**For `window_type`:**

* **MUST** accurately reflect the merchant's legal and commercial policy.
* **MUST** provide `return_days` if the type is `finite_window`.

**For `return_method.fee`:**

* **SHOULD** use `display_text` to explain the nature of the fee (e.g., "Prepaid Label", "Restocking Fee").
* **MUST** provide `amount` if the type is `fixed_fee`.

### Platform Responsibilities

Platforms **SHOULD** use return policies to answer buyer questions and provide assurance:

* Surface "Final Sale" warnings early in the checkout flow.
* Answer specific questions like "Is return shipping free?" by inspecting the `by_mail` method fee.
* Use `return_days` to calculate and display the specific return deadline based on the delivery date.

## Examples

### Mixed Cart

In this example, apparel items have a standard window, while a custom item is final sale.

```json
{
"return_policies": [
{
"id": "rp_apparel",
"line_item_ids": ["shirt", "pants"],
"window_type": "finite_window",
"return_days": 30,
"exchanges_allowed": true,
"methods": [
{
"type": "in_store",
"fee": {
"type": "free",
"display_text": "Free In-Store Return"
}
},
{
"type": "by_mail",
"fee": {
"type": "fixed_fee",
"amount": 500,
"display_text": "Return Shipping Fee"
}
}
]
},
{
"id": "rp_final_sale",
"line_item_ids": ["custom_engraved_watch"],
"window_type": "final_sale",
"exchanges_allowed": false
}
]
}
```
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ nav:
- Buyer Consent Extension: specification/buyer-consent.md
- Discounts Extension: specification/discount.md
- Fulfillment Extension: specification/fulfillment.md
- Return Extension: specification/return.md
- Cart Capability:
- Overview: specification/cart.md
- HTTP/REST Binding: specification/cart-rest.md
Expand Down
60 changes: 60 additions & 0 deletions source/schemas/shopping/return.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ucp.dev/schemas/shopping/return.json",
"name": "dev.ucp.shopping.return",
"title": "Return Extension",
"description": "Extends Checkout with return policy support.",
"$defs": {
"return_policy": {
Comment thread
venkatesh-ucp marked this conversation as resolved.
"$ref": "types/return_policy.json"
},
"return_method": {
"$ref": "types/return_method.json"
},
"return_fee": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To elaborate on @amithanda 's observation in 1.4, you can have this API be considerably more expressive for all parties if return_fee is return_fees and you split a new shape out (i.e. "return_fees": ReturnFee[]).

  • The ReturnFee shape can capture fee-type specific fields (i.e. amount for fixed_fee) through schema (subtyping, FixedReturnFee) rather than documentation.
  • You provide the business the option to convey more granular fee data. For example the return fee of $15 is attributable to a $5 shipping cost and a $10 bulky item restock.
  • You can represent "free" as an empty array.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per prior decisions, this fee is mostly part of the pre-purchase disclosure. It is mostly for display purposes and my goal has been to keep it as simple as possible and provide more flexibility to provide information to the merchant to make a decision. In this design, the platform is meant to mostly pass through this information to the user and use it for display purposes. All the complex behaviors around storing the data will be handled in the post-purchase PR.

For what you are asking, merchants can achieve this today using the existing schema in two robust ways:

  • Pre-computing the amount: Because return policies are exposed at checkout time where line item prices are fully established, the merchant backend will pre-compute the exact percentage fee in minor units (e.g., 15% of a $100 item = $15.00) and pass it as a fixed_fee with the explanation in display_text.
  • Disclosing the rule via display_text: If the fee cannot be pre-computed (e.g., due to pending freight calculation or potential defect inspection), the merchant can use customer_responsibility or fixed_fee (with base shipping) and explicitly state the percentage rule in display_text.
  • The display_text field provides the most flexibility in terms of messaging.

Example representation in existing schema:

"methods": [
  {
    "type": "by_mail",
    "fee": {
      "type": "fixed_fee",
      "amount": 1500,
      "display_text": "15% Restocking Fee ($15.00 pre-computed)"
    }
  }
]

"$ref": "types/return_fee.json"
},
"dev.ucp.shopping.checkout": {
"title": "Checkout with Return",
"description": "Checkout extended with return policies.",
"allOf": [
{
"$ref": "checkout.json"
},
{
"type": "object",
"properties": {
"return_policies": {
"type": "array",
"items": {
"$ref": "#/$defs/return_policy"
},
"description": "Return policies applicable to items in the checkout.",
"ucp_request": "omit"
}
}
}
]
},
"dev.ucp.shopping.return": {
"platform_schema": {
"title": "Return Capability (Platform)",
"description": "Platform-level return capability configuration",
"allOf": [
{
"$ref": "../capability.json#/$defs/platform_schema"
}
]
},
"business_schema": {
"title": "Return Capability (Business)",
"description": "Business-level return capability configuration",
"allOf": [
{
"$ref": "../capability.json#/$defs/business_schema"
}
]
}
}
}
}
23 changes: 23 additions & 0 deletions source/schemas/shopping/types/return_fee.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ucp.dev/schemas/shopping/types/return_fee.json",
"title": "Return Fee",
"description": "The cost structure associated with a specific return method.",
"type": "object",
"required": ["type"],
"properties": {
"type": {
"type": "string",
"enum": ["free", "fixed_fee", "customer_responsibility"],
Comment thread
venkatesh-ucp marked this conversation as resolved.
Outdated
"description": "The cost structure for the return method."
},
"amount": {
"$ref": "amount.json",
"description": "Fixed return fee charged by the merchant, represented in minor currency units (e.g., cents). Required if type is fixed_fee."
},
"display_text": {
"type": "string",
"description": "Human-readable text to display against the fee to provide context to the buyer (e.g., 'Restocking Fee', 'Return Shipping Label')."
}
}
}
19 changes: 19 additions & 0 deletions source/schemas/shopping/types/return_method.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ucp.dev/schemas/shopping/types/return_method.json",
"title": "Return Method",
"description": "The physical method through which the buyer can return the item.",
"type": "object",
"required": ["type", "fee"],
"properties": {
"type": {
"type": "string",
"enum": ["in_store", "by_mail", "kiosk"],
Comment thread
venkatesh-ucp marked this conversation as resolved.
Outdated
"description": "The physical method through which the buyer can return the item."
},
"fee": {
"$ref": "return_fee.json",
"description": "The cost structure associated with this specific return method."
}
}
}
39 changes: 39 additions & 0 deletions source/schemas/shopping/types/return_policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ucp.dev/schemas/shopping/types/return_policy.json",
"title": "Return Policy",
"description": "Conditions, methods, timelines, and costs associated with returning physical items.",
"type": "object",
"required": ["id", "line_item_ids", "window_type"],
"properties": {
"id": {
Comment thread
venkatesh-ucp marked this conversation as resolved.
Outdated
"type": "string",
"description": "Unique identifier for the return policy."
},
"line_item_ids": {
"type": "array",
"items": {
"type": "string"
},
"description": "Line items governed by this return policy, allowing distinct policies per item."
},
"window_type": {
"type": "string",
"enum": ["lifetime", "no_returns", "final_sale", "finite_window"],
"description": "The type of return window."
},
Comment thread
venkatesh-ucp marked this conversation as resolved.
Outdated
"return_days": {

@gsmith85 gsmith85 May 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return_days works as a policy statement at checkout, but to answer "can I still return this?", a platform has to join checkout-time return_days against a delivered event in fulfillment.events on the Order or rely on its own transaction records.

Consider also extending Order with a return_deadline. The merchant computes it relevant to whatever temporal anchor applies (i.e. order placed, fulfillment, a specific unrelated date) and the platform just reads it.

This handles non-retail domains naturally, a hotel cancellation (24h before check-in) has a different anchor than delivery entirely. return_days can't express that, but a pre-computed UTC deadline can.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted some clarifications on how you expect this to work before I go ahead and make the changes.

Option 1: As you mention, have the platform store the return policy or persist it in the Order and then calculate it whenever the fulfillment.events (delivered) is received given that majority return policies will kick in after the item is delivered (physical or digital). This has the complexity that the platform needs to calculate this number.

Option 2: Add a field to the line_items part of the Order which the merchant updates whenever a fulfillment event (delivered) occurs. The platform the looks up the data to answer the "can I still return this?" question. Line items vs a global deadline for the Order since the return deadlines could be different based on the category of the product. for eg: electronics v/s clothing.

Are we on the same page that option 2 is how this should be implemented?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for the missed message here, let me attempt to clarify the premise first.

At checkout a business may communicate a policy. For example, "free returns for 30 days." The Platform is poorly positioned to translate this into an actual deadline which is the most relevant piece of information for the customer post-purchase.

The platform is poorly positioned because in the existing schema, the only anchor provided to the platform is the time checkout occurred. But modern ecommerce is more complicated than that, for physical goods it may be (i) 30 days from purchase (ii) 30 days from fulfillment or (iii) 30 days from delivery. Amazon and Target use (iii), Sephora is (i).

This brings us to the options you outlined.

Option 1. We should avoid this as it forces the platform to calculate the returns deadline against the Policy which has insufficient fidelity. Providing the platform a referential hint that captured "duration from purchase/fulfillment/delivery" may be useful in any case but doesn't solve the undesirable outcome. In the case of fulfillment or delivery, it necessitates the platform track post-purchase notifications which is onerous.

Option 2. Allows the business to serve as the source of truth which is directionally aligned to the protocol. The Order becomes the authoritative conveyance for which items have which return deadlines for which line_items. This allows the business to provide a more contextualized signal than what was shown at Checkout time.

I'm aligned to Option II and it looks like the group has similarly weighed in — @sbeashwar called for extending Order with return_policies[] given the checkout TTL gap, and @amithanda's recommended spec prose already references the Order as the authoritative source. Is that going to be tracked as part of this PR or as follow-up?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm good with Option II. The post order experience will be handled in a follow up PR. We should be filing the RFC for that next week'ish.

"type": "integer",
"description": "Number of days allowed for a return, typically starting from the date of delivery. Required if category is finite_window."
Comment thread
venkatesh-ucp marked this conversation as resolved.
Outdated
},
"exchanges_allowed": {
"type": "boolean",
"description": "Indicates whether the buyer can exchange the item."
},
"methods": {
"type": "array",
"items": { "$ref": "return_method.json" },
"description": "Permitted physical methods for returning the item, along with their associated fee structures."
}
}
}