Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .linkignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Ignore ucp.dev dummy links
https://ucp\.dev/specification/reference\?v=2026-01-11
https://ucp\.dev/specification/reference\?v=2026-01-11
241 changes: 241 additions & 0 deletions docs/specification/return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
<!--
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, 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` registry to Checkout and a reference on line items:

- `line_items[].return_policy_id` — reference to the policy that applies to this item.
- `return_policies` — map of policy definitions keyed by ID, containing:

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_policies likely need to be polymorphic or use a oneOf to capture the period as either a duration or a deadline.

  • Duration: "30-day return window" (retail)
  • Deadline: "free cancellation until 2026-07-30T00:00:00Z" (travel, events)

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.

SGTM, pending TC alignment on introduction of Duration.

- `return_period_in_days` — the number of days allowed for the return.

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.

If we do use duration, we should consider preferring JSON Schema's duration built-in type versus granularity post-fixing (i.e._in_days).

This is consistent with AIP-142 and create a more flexible API that can handle fractional day granularity as the protocol extends to other verticals.

Requires TC discussion, but we should likely add this guidance to our schema authoring guidelines.

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 this. I can add it in if y'all have the discussion in the TC and agree on it or we can always do this and the suggestion in the below comment in a follow up PR.

- `supported_resolutions[]` — allowed outcomes (refund, exchange, etc.).
- `methods[]` — permitted physical methods (in-store, by-mail, etc.)
- `fee` — the cost structure for that specific method.
- `reason` — human-readable explanation of why this policy applies.
- `policy_url` — URL to the merchant's full return policy document.
Comment thread
venkatesh-ucp marked this conversation as resolved.

### Relationship to Other Extensions

The Return Extension is independent of the Fulfillment Extension. Businesses MAY use information from `fulfillment` to compute return options (e.g., in-store return locations may correlate with fulfillment retail locations), but this extension does NOT model that relationship. Platforms MUST NOT assume any structural correlation between return methods and fulfillment methods.

### Scope and Future Directions

- **Pre-Purchase Disclosure Only**: This extension addresses pre-purchase disclosure of return policy only. Post-purchase return initiation, label generation, refund processing, and exchange execution are out of scope. A future `dev.ucp.shopping.return_action` extension may address these workflows; implementers MUST NOT extend this schema for those purposes.
- **Product-Level Returns**: A return policy is fundamentally a property of a product (or its category), not a property of a checkout. A separate Product or Catalog capability could expose return policies at the catalog level in the future. For v1, checkout-time exposure is used for simplicity.

**Mental model:**

- **Line Items:**
- 👕 (Shirt) -> `return_policy_id` = `"rp_standard"`
- 👖 (Pants) -> `return_policy_id` = `"rp_standard"`
- ⌚ (Watch) -> `return_policy_id` = `"rp_final_sale"`
- **Return Policies Registry:**
- `"rp_standard"`:
- `return_period_in_days` = 30 Days 🗓️
- `supported_resolutions` = `["original_payment_method", "exchange"]`
- `methods`: In-Store (free) 🏬, By Mail ($5.00) 📦
- `"rp_final_sale"`:
- `supported_resolutions` = `[]` (Final Sale)
- `reason` = "Clearance items are non-returnable." 🚫

## Discovery

Businesses advertise return policy support in their profile by registering the
extension under `capabilities`:

```json
{
"ucp": {
"version": "{{ ucp_version }}",
"capabilities": {
"dev.ucp.shopping.return": [
{
"version": "{{ ucp_version }}",
"extends": ["dev.ucp.shopping.checkout"],
"spec": "https://ucp.dev/{{ ucp_version }}/specification/return",
"schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/return.json"
}
]
}
}
}
```

Platforms SHOULD check for this capability before attempting to render
`return_policies` from a checkout response.

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') }}

### Entities

#### Return Policy

{{ extension_schema_fields('return.json#/$defs/return_policy', 'return') }}

#### Return Method

{{ extension_schema_fields('return.json#/$defs/return_method', 'return') }}

#### Return Fee

{{ extension_schema_fields('return.json#/$defs/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_period_in_days` | No | Quantitative window for the return. |
| `return_policy` | `reason` | No | Human-readable explanation of policy application. |
| `return_policy` | `policy_url` | No | URL to the merchant's full return policy document. |
| `return_policy` | `supported_resolutions` | No | Allowed outcomes (refund, exchange, etc.). |
| `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 Policy References:**

- **MUST** ensure every `return_policy_id` on a line item resolves to a key present in the top-level `return_policies` registry.

**For Contextual Accuracy:**

- **SHOULD** use contextual information available in the customer's checkout session (e.g., provisional buyer location signals such as shipping destination) to provide the most accurate policy information possible rather than relying on static defaults.

**For Policy Locking:**

- **MUST** honor the return policy that was in effect at the time of order placement; that policy MUST be sourced from the Order resource (when available) rather than recomputed from current merchant rules. The return policy returned at checkout response time is the policy in effect for the buyer at that moment.

**For `return_period_in_days`:**

- **MUST** accurately reflect the merchant's legal and commercial return window
duration.

**For `supported_resolutions`:**

- **MUST** list all resolutions applicable to the `line_items` in the checkout session governed by that policy (e.g., if exchange is supported for those items, it must be explicitly listed).

**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_period_in_days` to calculate and display the specific return
deadline based on the delivery date.

**For Partial Quantity Returns:**

- **SHOULD** assume return policies apply to individual units within a line item. Unless specified otherwise (e.g., for bundled items), any subset of quantities is subject to the same return policy.

**For Absent Policy Data:**

- **MUST NOT** infer items are non-returnable if the Return capability is advertised in discovery but `return_policies` is absent or empty in a checkout response. Platforms SHOULD fall back to general merchant return info (e.g., via the merchant's profile URL).
- **MUST** treat unresolved policy references (`return_policy_id` without a matching key in `return_policies`) as having no stated return policy (per the Absent Policy Data rule) and **MUST NOT** infer the item is non-returnable.

**Interpretation of Policy States:**

Platforms MUST interpret combinations of `methods` and `return_period_in_days` as follows:

- `methods` absent or empty (`[]`): Treat as "no return methods supported" (i.e., non-returnable/final sale).
- `return_period_in_days` is `0`: Treat as "non-returnable" (final sale).
- `return_period_in_days` absent, but `methods` present: Treat as "no time limit on returns".
- `return_period_in_days` present, but `methods` absent/empty: Ambiguous. Interpret as returnable, but client/buyer must arrange their own return shipment, or consult the full policy link if provided.

## Examples

### Mixed Cart

In this example, apparel items reference a standard policy, while a custom item references a final sale policy with a reason.

```json
{
"line_items": [
{
"id": "shirt",
"return_policy_id": "rp_standard"
},
{
"id": "pants",
"return_policy_id": "rp_standard"
},
{
"id": "custom_engraved_watch",
"return_policy_id": "rp_final_sale"
}
],
"return_policies": {
"rp_standard": {
"return_period_in_days": 30,
"policy_url": "https://example.com/returns",
"supported_resolutions": ["original_payment_method", "exchange"],
"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"
}
}
]
},
"rp_final_sale": {
"supported_resolutions": [],
"reason": "Custom engraved items are final sale."
}
}
}
```
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
125 changes: 125 additions & 0 deletions source/schemas/shopping/return.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
{
"$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.
"title": "Return Policy",
"description": "Conditions, methods, timelines, and costs associated with returning physical items.",
"type": "object",
"properties": {
"return_period_in_days": {
Comment thread
venkatesh-ucp marked this conversation as resolved.
"type": "integer",
"minimum": 0,
"description": "Length of the merchant's return window in days, measured from the date of delivery. This is a policy statement — it communicates the window duration, not a pre-computed deadline. A value of 0 explicitly indicates the items are non-returnable (final sale)."
},
"supported_resolutions": {
"type": "array",
"items": {
"type": "string",
"description": "The resolution method. Well-known values: `original_payment_method` (refund to original payment method), `store_credit` (refund for store credit), `gift_card` (refund as gift card), `exchange` (exchange for different item), `replacement` (replace with same item). Clients MUST tolerate unknown values."
},
"description": "Supported outcomes for the return. An empty array `[]` explicitly indicates the items are non-returnable (final sale)."
},
"methods": {
"type": "array",
"items": {
"$ref": "#/$defs/return_method"
},
"description": "Permitted physical methods for returning the item, along with their associated fee structures."
},
"reason": {
"type": "string",
"description": "Human-readable explanation of why this return policy applies. Helps platforms and agents explain policy provenance to buyers (e.g., 'Custom engraved items are non-returnable per our personalization policy'). Optional."
},
"policy_url": {
"type": "string",
"format": "uri",
"description": "URL to the merchant's full return policy document. Platforms SHOULD surface this link alongside the structured policy data so buyers can review complete terms."
}
}
},
"return_method": {
"title": "Return Method",
"description": "The physical method through which the buyer can return the item.",
"type": "object",
"required": ["type", "fee"],
"properties": {
"type": {
"type": "string",
"description": "Physical return channel. Clients MUST tolerate unknown values. Well-known values: `in_store` (buyer returns at a retail location), `by_mail` (buyer ships item back via carrier), `kiosk` (buyer drops off at a self-service kiosk or designated drop point).",
"examples": ["in_store", "by_mail", "kiosk"]
},
"fee": {
"$ref": "#/$defs/return_fee",
"description": "The cost structure associated with this specific return method."
}
}
},
"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)"
    }
  }
]

"title": "Return Fee",
"description": "The cost structure associated with a specific return method.",
"type": "object",
"required": ["type"],
"properties": {
"type": {
"type": "string",
"description": "Cost structure for this return method. Clients MUST tolerate unknown values. Well-known values: `free` (merchant covers return costs), `fixed_fee` (flat fee charged, see amount), `customer_responsibility` (buyer arranges and pays for return shipping).",
"examples": ["free", "fixed_fee", "customer_responsibility"]

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.

In the above model, customer_responsibility a VariableFee that has the description of "Customer assumed shipping cost." or the like.

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.

Leaving this unresolved since it related to the above comment.

},
"amount": {

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.

It is fairly common for restocking fees to structure as a percentage of the total cost rather than a fixed amount.

For example on Kohl's for freighted delivery:

- Shipping costs and surcharges are non-refundable.
- There is a 15% restocking fee unless the returned item is defective.

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.

Leaving this unresolved since it related to the above comment.

"$ref": "types/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')."
}
}
},
"dev.ucp.shopping.checkout": {
"title": "Checkout with Return",
"description": "Checkout extended with return policies.",
"allOf": [
{
"$ref": "checkout.json"
},
{
"type": "object",
"properties": {
"return_policies": {
"type": "object",
"additionalProperties": {
"$ref": "#/$defs/return_policy"
},
"description": "Registry of return policies, keyed by policy ID. Line items reference these policies by key.",
"ucp_request": "omit"
},
"line_items": {
"type": "array",
"items": {
"allOf": [
{
"$ref": "types/line_item.json"
},
{
"type": "object",
"properties": {
"return_policy_id": {
Comment thread
venkatesh-ucp marked this conversation as resolved.
"type": "string",
"description": "ID of the return policy that applies to this line item. Must reference a key in the top-level return_policies map.",
"ucp_request": "omit"
}
}
}
]
}
}
}
}
]
}
}
}
Loading