-
Notifications
You must be signed in to change notification settings - Fork 431
feat: add Return Extension to UCP specification #257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3119aa9
1d11161
135ba78
28e20b0
e222826
87a7de0
e5990c8
5ee68ab
3746447
c53150e
5768e2e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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: | ||
| - `return_period_in_days` — the number of days allowed for the return. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. 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.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
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. | ||
|
|
||
|
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 | ||
|
venkatesh-ucp marked this conversation as resolved.
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." | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| 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": { | ||
|
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": { | ||
|
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": { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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"] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the above model,
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Leaving this unresolved since it related to the above comment. |
||
| }, | ||
| "amount": { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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": { | ||
|
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" | ||
| } | ||
| } | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return_policieslikely need to be polymorphic or use aoneOfto capture the period as either a duration or a deadline.There was a problem hiding this comment.
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.