feat: add Return Extension to UCP specification#257
Conversation
This change introduces the Return Extension, allowing businesses to communicate return conditions, methods, timelines, and costs. Key changes: - Added JSON schemas for Return Policy, Method, and Fee. - Extended Checkout and Order with return policies. - Renamed Return Policy 'category' to 'return_window_type'. - Added specification documentation and updated site navigation. Closes #TBD
| "enum": ["lifetime", "no_returns", "final_sale", "finite_window"], | ||
| "description": "The type of return window." | ||
| }, | ||
| "return_days": { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Consolidates the Return Extension domain schemas by embedding return_policy, return_method, and return_fee directly into inline $defs within return.json, eliminating redundant standalone files. Additionally, simplifies policy modeling by completely removing the brittle window_type property to rely natively on clean field semantics, converts logistics channels and fee structures to open strings for robust client parsing, makes policy correlation IDs optional, and adds a standard capability profile Discovery section to the specification documentation.
|
The schema covers the physical logistics of a return well (channel, fees, timeline) but doesn't address what happens financially once the return is accepted (refund semantics). In practice, merchants offer different refund options: original payment method, store credit, or exchange/replacement only. These vary per policy and materially affect purchase decisions. How about adding a field like refund_type on return_policy (with well-known values such as original_payment, store_credit, replacement_only) would let an agent answer "what do I actually get back if I return this?" Which is distinct from how the item physically gets back to the merchant. |
|
Thanks @venkatesh-ucp for doing a pass over some basic structural issues. I did another pass focussing over the overall architectural shapes, granularity gaps and behavior specification of those gaps. Although the Return Extension is well-scoped for what it tries to do — pre-purchase return policy disclosure at checkout — and the recent revisions have addressed most of the structural concerns. However, the current schema models returns as an item-by-item assignment, which doesn't reflect how merchants actually express return policies in production. Most return policy authoring is hierarchical (site → category → brand → item), and the schema currently forces all of that hierarchy to be flattened into per-line-item bindings on every checkout response. Three categories of suggestions:
Here are some specific recommendations related to these, to make this more robust: Part 1 — Schema Architecture1.1 The relationship is inverted: items should reference policies, not the reverseThe current design: "return_policies": [
{
"id": "rp_apparel",
"line_item_ids": ["shirt", "pants"],
"return_days": 30,
...
}
]Policies own a list of items. To find a given item's policy, you scan the array. To deduplicate when the same policy covers 50 items, you can't — there's only one place to bind line items. Why this matters: Returns are an attribute of an item, not a grouping of items. The current pattern works for fulfillment because fulfillment IS a grouping primitive — items genuinely travel together as a shipment. Return policy is not a grouping primitive — each item has a single return policy that applies to it regardless of what other items are in the cart. Recommendation: Move toward a keyed-map registry pattern where policies are defined once and referenced from line items: {
"return_policies": {
"rp_standard": {
"return_days": 30,
"exchanges_allowed": true,
"methods": [...]
},
"rp_final_sale": {
"exchanges_allowed": false
}
}
}And on the line item side (via a line_item extension): "line_items": [
{ "id": "li_1", "item": {...}, "return_policy_id": "rp_standard" },
{ "id": "li_2", "item": {...}, "return_policy_id": "rp_final_sale" }
]Benefits:
Trade-off: This requires extending 1.2 The schema can't express policy hierarchy or authoring intentReal merchant return policies are authored at multiple levels:
The current schema flattens all of this into runtime per-item assignments. A buyer asking "Why is this final sale?" gets no answer beyond "the merchant said so" — the schema can't communicate "because this item is custom engraved" or "because this falls under our jewelry category policy." Why this matters for AI agents: The Overview explicitly cites use cases like "An AI agent proactively answers questions like 'Can I return this if it doesn't fit?'" The agent's value here is in explaining WHY a policy applies, not just stating it. Without policy provenance, the agent is reduced to reading flat data. Recommendation: Add a "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', 'Electronics carry our 15-day return window'). Optional.",
}This costs nothing for merchants who don't have authoring context, and unlocks meaningful explanation for those that do. 1.3 The policy primitive conflates policy definition with policy bindingThe current
These are different concerns. Mixing them means:
This is a downstream consequence of inverting the relationship (Section 1.1) — if items reference policies, then the policy object naturally becomes pure definition with no binding data. 1.4 Restocking fee modeling is awkwardThe current schema places "methods": [
{ "type": "in_store", "fee": { "type": "fixed_fee", "amount": 500, "display_text": "Restocking Fee" } },
{ "type": "by_mail", "fee": { "type": "fixed_fee", "amount": 500, "display_text": "Restocking Fee" } }
]Conversely, a merchant who has "free returns by mail with a $5 restocking fee" can't express that — Real-world fee structure for returns typically has two independent dimensions:
Recommendation: Split the fee model into logistics and restocking concerns: "methods": [
{
"type": "by_mail",
"shipping_cost": { "type": "free" }, // logistics
"restocking_fee": { "amount": 500 } // separate from logistics
}
]If splitting is too invasive for v1, at minimum the spec should acknowledge: "When a merchant charges a restocking fee independent of return method, businesses SHOULD apply the same 1.5
|
| State | Intended meaning? |
|---|---|
methods absent |
? |
methods: [] |
? |
return_days: 0 |
Item is returnable for zero days? Or not returnable? |
return_days absent, methods present |
Returnable with no time limit? |
return_days: 30, methods absent |
Returnable but no methods specified? |
A platform reading any of these has no normative basis for interpretation. The spec needs a state table making these explicit. Suggested defaults:
methodsabsent ORmethods: []→ MUST treat as "no return methods supported"return_days: 0→ MUST treat as "non-returnable" (equivalent to missing the policy)return_daysabsent withmethodspresent → MUST treat as "no time limit on returns"return_days: 30with nomethods→ ambiguous; spec MUST specify
3.4 Relationship to Fulfillment is structurally implicit
The Return extension is independent of the Fulfillment extension, but in practice they're entangled:
- "In-store returns at any Nordstrom location" depends on the same retail location data that fulfillment uses
- Carrier-paid return labels depend on which carrier delivered the item
- Cross-border returns depend on the destination country in fulfillment
By keeping them decoupled, the schema avoids forcing a dependency, which is the right architectural choice. But the spec should explicitly call out the implicit relationships:
"The Return Extension is independent of the Fulfillment Extension. Businesses MAY use information from
fulfillmentto 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."
3.5 No explicit handling of the "no return policy data at all" state
When return_policies is absent from a checkout, what should a platform display?
- "This merchant supports return policy data, but didn't provide any for this checkout" → suggests no items are returnable
- "This merchant doesn't support return policy data" → suggests platform should fall back to merchant URL
The Discovery section tells platforms to check the capability before rendering, which partially addresses this. But the spec should still explicitly say:
"When the Return Extension capability is advertised in discovery but
return_policiesis absent or empty in a specific checkout response, platforms MUST NOT infer the items are non-returnable. The merchant simply has no return policy data to expose via this protocol for this checkout."
3.6 Where is the terms link?
Legal return terms are typically lengthy and link out to a full policy page. The schema has no way to:
- Link to a full policy document
- Indicate "see merchant's terms for restrictions"
- Disclose exceptions that can't be modeled (state-specific overrides, etc.)
Recommendation: Add an optional policy_url at the policy level:
"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."
}Part 4 — Forward-Looking Architectural Notes
4.1 Return initiation is out of scope — make this explicit
This extension is pure pre-purchase disclosure. It does NOT model:
- How a buyer initiates a return
- How the platform receives an RMA number
- How return labels are generated
- How refunds are processed
This is the correct scope for v1, but the spec should explicitly state it AND reserve the contract for future extensions:
"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_actionextension may address these workflows; implementers MUST NOT extend this schema for those purposes."
This prevents future PRs from mutating this extension to add post-purchase semantics that don't belong here.
4.2 Consider whether returns belong at Product, not Checkout
A return policy is fundamentally a property of a product (or its category), not a property of a checkout. The checkout-time exposure is for buyer disclosure, but the authoritative source of return policy is the catalog.
Currently, every checkout response re-computes and re-sends the full return policy for every line item, even though the policy hasn't changed. This is wasteful for large carts.
Future direction (out of scope for this PR but worth noting in the spec):
A separate Product or Catalog capability could expose return policies at the catalog level, with the Return Extension on Checkout becoming a simple confirmation: "These items have been validated against current policies; here is the effective set."
For v1, the checkout-time exposure is fine. But the spec should acknowledge that product-level return policy disclosure is a likely future direction so that the Checkout extension doesn't become the de facto only path.
In my opinion, the current version of the spec tackles the That leaves us with refund to the original form of payment. That should be the default and I don't think it needs to be tracked separately. I also expect this to fit in more with the return initiation flow/capability and should be considered out of scope for this PR. |
|
Read through the recent revisions - thoughtful set of fixes. A few points from an agent perspective: 1. Extend Today Two distinct gaps: a) Long-tail lookup. When the buyer asks "can I still return this?" days or weeks post-delivery, the Checkout is gone. Without state-mirroring at completion time, the agent has nothing to read. b) Locked policy retrieval. amithanda's Part 3.1 says the locked-at-purchase policy "MUST be sourced from the Order resource (when available)" - that MUST only works if Order carries Cleanest: extend 2. Rename Current name parses ambiguously ("what does 30 return_days mean?"). Putting the unit in the name resolves it. Stripe uses 3. Refund options are richer than Building on prasad-stripe's
All five render identically against Suggest "refund_methods": [
{ "type": "original_payment_method" },
{ "type": "store_credit" },
{ "type": "gift_card" },
{ "type": "replacement_only" }
]Open string with well-known values, same convention as 4. Drop the empty capability schemas. The capability defines Return policies are pure response data; no pre-checkout negotiation flag is needed. Follow the |
|
Created a new field called supported_resolutions field that addresses your use case and removed the exchanges_allowed field altogether since they achieve the same goal. |
|
@amithanda Thanks for the detailed comments! I've responded inline.
I have implemented this change. To provide more context as to why I picked the previous design. My assumption was that cart sizes are usually small. One or two items or whatever might be needed to meet the free shipping threshold. In that case linking the policy to the line item made more sense since the platform didn't have to deal with the mapping logic of going from a policy list to a line item nor did merchant have to do any work to make it available in that format. However, I think all of this changes if the cart is larger. The one example I could think of is a grocery store shopping cart or a cart that has line items/products of multiple different categories (electronics + home decor) with vastly different policies. I think this design gives most flexibility for those cases.
I have implemented this recommendation.
This was implemented in 1.1 and a previous change.
I have decided to leave this out of scope for this PR since this i think this is crossing the lines into returns initiation which will be handled soon in a follow up PR. I want to focus on what the user needs to know when they want to make a purchase decision and it is my opinion that as an user it is most important to understand if there is a restocking fee or whether it is their responsibility to pay for the shipping back. Calculating the shipping cost back ahead of time will not be a deal breaker, the fact that they have to pay for returns will be the deal breaker. Lastly, the merchant has a display text available to them to communicate this information if needed.
I'm keeping this out of scope for this PR since I agree it might be crossing into the post-purchase territory which will be covered in a follow up PR soon.
After moving to the map of return policies linked to the line items via keys, this suggestion is already implemented. I have not gone with allowing a default policy using a standardized default keyword because I feel that is more error prone and with the new architecture, the merchant can achieve this by defining a default keyed policy and use that key in all the line items.
Add to the list of out of scope items.
Add to the list of out of scope items.
I have applied all the six recommendations from this section.
I've applied both the recommendations from this section. |
|
Thanks @venkatesh-ucp for all the work here. This has come a long way and the latest revision is in great shape. A few things worth calling out as real improvements:
Remaining comments are narrow consistency items, not design problems. |
Thank you for all the reviews so far! |
|
|
||
| - `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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| "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": { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Leaving this unresolved since it related to the above comment.
| } | ||
| } | ||
| }, | ||
| "return_fee": { |
There was a problem hiding this comment.
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
ReturnFeeshape can capture fee-type specific fields (i.e.amountforfixed_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.
There was a problem hiding this comment.
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 afixed_feewith the explanation indisplay_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 usecustomer_responsibilityorfixed_fee(with base shipping) and explicitly state the percentage rule indisplay_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)"
}
}
]| "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"] |
There was a problem hiding this comment.
In the above model, customer_responsibility a VariableFee that has the description of "Customer assumed shipping cost." or the like.
There was a problem hiding this comment.
Leaving this unresolved since it related to the above comment.
| "enum": ["lifetime", "no_returns", "final_sale", "finite_window"], | ||
| "description": "The type of return window." | ||
| }, | ||
| "return_days": { |
There was a problem hiding this comment.
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?
| 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: |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
SGTM, pending TC alignment on introduction of Duration.
- Rename `return_days` to `return_period_in_days` for clarity. - Replace `exchanges_allowed` boolean with `supported_resolutions` flat array of strings to support richer outcomes (e.g. store credit, gift card). - Drop empty capability schemas (`dev.ucp.shopping.return`). - Update documentation and examples to match schema changes.
b2ad137 to
5ee68ab
Compare
igrigorik
left a comment
There was a problem hiding this comment.
Catching up, a lot of great points came up in the reviews. With the benefit of fresh eyes on the problem, I want to step back and reason through the properties of the domain we're modelling... bear with me, please! 🙇🏻
What a merchant can promise vs. what is resolved later
A merchant can publish a policy about returns ahead of time. However, fine-grained resolution (methods available, fees, refund destination, label source, who-pays-what) depends on inputs that don't yet exist at cart or checkout:
- Buyer's reason (remorse vs defect, wrong item, didn't fit, ...)
- Item condition (new / damaged in transit / worn)
- Return ship-from location (distinct from the original ship-to)
- Payment instrument actually used (split tenders, gift card, ...)
- Buyer's resolution preference (refund / exchange / store credit)
- Item vs order-level policies (order thresholds, bundles, bogos, ...)
- ... the list goes on
I think this maps and suggests two distinct surfaces:
- Pre-purchase policy (Catalog / Cart): roughly, "if you returned this under the worst case, here is what to expect."
- Post-purchase resolution: "given this reason + condition + location + preference, the policy is..."
Most of the concerns raised in the review trace back to one root: the proposal places post-purchase-resolution fields (methods, fees, supported resolutions, refund destinations) on a pre-purchase surface, where they can't be reliably authored. The data needed to populate them doesn't yet exist, and the business policy is often complex and dynamic -- i.e. it's not something that a business would publish or enumerate as static facts.
Implication: I think the pre-purchase contract needs to be MUCH narrower than the proposal carries today, and post-purchase resolution needs a much richer negotiation surface (not a static policy). The same extension can extend and cover both, and we can and should tackle top of funnel first and separately.
This isn't just returns, we should aim to solve the meta "policy" problem
"Structured policy, per-item targetable, lifecycle tied to a purchase event" describes a class. Refund (return + cancellation), warranty, and subscription terms all share similar shape:
- Each is a structured, machine-readable claim a merchant publishes ahead of time
- Each can attach cart-wide or per-item
- Each gets locked at purchase and travels with the order
- Each has its own dynamic resolution exchange after the fact
I think we should step back and think how we can chisel out a reusable primitive rather than a returns s one-off. In other words, something like dev.ucp.shopping.policies, which can carry refund, warranty, subscription, and so on.
Discount capability has a shape that fits, and we should take inspiration from it
The discount extension already solved a very similar problem. discount.applied[] carries structured per-discount entries, and each entry's allocations[].path uses JSONPath to attribute the discount to specific line items, totals, or the cart structure. The key thing with that pattern is that it allows rich targeting and avoids duplication -- you can target the whole cart, you can target sublines, without repeating same data on each line.
Napkin sketch
Worth calling out that we already have links[] that carry policy information. links[] stays as it is — the always-present, untargeted URL-shaped fallback (privacy, ToS, FAQ, refund_policy URL). But, when dev.ucp.shopping.policies is negotiated, a new policies[] object (extends catalog, cart, checkout) can carry typed structured policies alongside it. Body fields are deliberately omitted here, focusing on the macro shape:
"line_items": [...],
...
"policies": [
{
"type": "refund", // no applies_to means cart/catalog-wide default
"id": "default",
"url": "https://example.com/returns"
// body fields (window, cancellation, ...) — TBD per type
},
{
"type": "refund",
"id": "engraved-final-sale",
"applies_to": ["$.line_items[2]"],
"final_sale": true
},
{
"type": "warranty",
"id": "std-warranty",
"url": "https://example.com/warranty"
}
]Targeting reuses JSONPath against the catalog/cart/checkout structure, matching the convention established by discount.allocations[].path. Omit applies_to → applies cart-wide. final_sale: true is shown as a hint at the kind of upper-bound, pre-purchase-honest field that belongs in a body; the actual body schema per type, and conflict-resolution rules when targets overlap, we can define if the overall approach makes sense.
I'm deliberately omitting a lot of detail in the sketch above. Stepping back:
- I think we're trying to model facts that cannot be modelled accurately at catalog/cart/checkout. To be clear, I'm not saying we cannot express some structured properties about refund/return policies, but that we can't overreach at this stage because resolution is dynamic and requires context that we do not have.
- I would like to propose that we explore the generic construct for policies, as outlined above.
Would love to hear thoughts and feedback. Happy to put together a more detailed proposal for (2) if we think it's a good direction to explore.
|
@igrigorik Thanks for laying this out—these are really strong architectural points. I thought about the tension between providing a structured While I agree that the ultimate source of truth for financial math and eligibility lies in the merchant's backend, I want to push back slightly on abstracting Returns into a generic policy container. Here are some counter-arguments that we should consider, as always looking forward to a good discussion around this 😄 : 1. The Upfront Promise vs. Dynamic Resolution 2. Are Returns, Warranties, and Subscriptions really the same "Meta-Policy"?
3. The Discount Analogy Breaks Down on "JSONPath Attribution" |
Agree. The meta point is that we can define a polymorphic base that will allow us to model different types of policies via shared mechanism. This does not mean that we cannot or should not define structured shapes for specific policies; we can model a refund/return policy with appropriate fidelity that makes sense. This is similar to how we modelled
You're right that those can be products / line-items, but that's not what this primitive is describing. The job of policy object is to highlight applicable policies for negotiated products -- e.g. the items you added to your cart all have 30-day return and 1-year warranty. When you add a subscription to your cart, a subscription cancellation policy appears in the set.
I disagree. It's very common to have cart-wide discounts (most common type, in fact), and policies frequently differ from item to item based on type of good, season/promo status, etc. One of the big reasons we adopted jsonpath approach for discounts is precisely because it allowed us to express these configurations in a much more compact way - a cart-wide discount is a single object, instead of being replicated on every object in the cart; a targeted discount is a single object just as it would be on the line itself. I agree that indirection has a cost, but that's a design pattern and decision we've already adopted in many places, and I think it fits here as well. @amithanda stepping back, I think we're actually very close in what we're saying from both sides, I'm just arguing for reusing the established design patterns and contracts in UCP. First, we need to more clearly separate (as you already suggested in earlier review) pre-purchase vs. return action operations (methods, fees, supported resolutions are return action primitives), and then examine how the pre-purchase facts can be expressed in a well-modeled and extensible way. |
|
@venkatesh-ucp @amithanda draft of an alternate design sketched out above: #572 — ptal. |
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.
Motivation
Currently, the UCP Fulfillment Extension only handles how physical goods get to a buyer (shipping, pickup, and associated options). However, returning items is a core primitive of physical commerce.
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.
Use Cases
This schema addition is designed to support the following agentic and platform use cases:
Detailed Design
Architectural Discussion: Fulfillment Extension vs. Standalone Extension
One of the main questions to answer before adding return policy support is around whether to embed
return_policieswithin the existing Fulfillment Extension. Specifically, the question is whether returns are a subset of physical fulfillment, or if they represent a broader conceptual primitive (a "policy") that applies to line items independent of logistics.Pros of Adding to the Fulfillment Extension
line_item_ids. Reusing this extension avoids duplicating the boilerplate required to map rules to specific items.Cons of Adding to the Fulfillment Extension (The Case for a Standalone Extension - Preferred approach)
return_policyis tightly coupled to the Fulfillment Extension, it becomes much harder to reuse the returns spec in other contexts. For example, a post-purchaseOrdercapability or a simple product discovery agent might need to display return policies without carrying the irrelevant baggage of forward-fulfillment routing and shipping rate calculations.Schema Structure & Discovery
return_policy,return_method,return_fee) are natively embedded into inline$defswithinreturn.jsonrather than split into standalone shared files.dev.ucp.shopping.returnextendingdev.ucp.shopping.checkout.Proposed Entities
Return Policy Response
return_period_in_dayspolicy_urlreasonsupported_resolutionsoriginal_payment_method,store_credit,gift_card,exchange,replacement. An empty array[]explicitly indicates the items are non-returnable (final sale).methodsExtended Checkout Line Item
This extension adds
return_policy_idto the standard CheckoutLineItementity.return_policy_idreturn_policiesmap.Return Method Object
typeWell-known values:
•
in_store: The buyer returns the item physically at a retail store location.•
by_mail: The buyer packages and ships the item back to the merchant via a carrier.•
kiosk: The buyer drops the item off at a designated self-service kiosk or drop-off point.feeReturn Fee Object
typeWell-known values:
•
free: The merchant covers return costs (prepaid label or free in-store drop-off).•
fixed_fee: The merchant charges a flat fee (restocking fee or deducted return label fee).•
customer_responsibility: The buyer arranges and pays for return postage entirely.amounttypeisfixed_fee.display_textExample Payload
{ "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." } } }Out of Scope for this PR
The following recommendations from the architectural review are explicitly out of scope for this PR:
label_provided) are deferred to post-purchase execution extensions.Checklist