diff --git a/.linkignore b/.linkignore index 2f00a358e..29fe84c6f 100644 --- a/.linkignore +++ b/.linkignore @@ -1,2 +1,2 @@ # Ignore ucp.dev dummy links -https://ucp\.dev/specification/reference\?v=2026-01-11 \ No newline at end of file +https://ucp\.dev/specification/reference\?v=2026-01-11 diff --git a/docs/specification/return.md b/docs/specification/return.md new file mode 100644 index 000000000..7ece6a868 --- /dev/null +++ b/docs/specification/return.md @@ -0,0 +1,241 @@ + + +# 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. + - `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. + +### 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. + +## 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 + +**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." + } + } +} +``` diff --git a/mkdocs.yml b/mkdocs.yml index 563de73a4..47185d6c6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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 diff --git a/source/schemas/shopping/return.json b/source/schemas/shopping/return.json new file mode 100644 index 000000000..bc212084c --- /dev/null +++ b/source/schemas/shopping/return.json @@ -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": { + "title": "Return Policy", + "description": "Conditions, methods, timelines, and costs associated with returning physical items.", + "type": "object", + "properties": { + "return_period_in_days": { + "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": { + "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"] + }, + "amount": { + "$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": { + "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" + } + } + } + ] + } + } + } + } + ] + } + } +}