feat!: Introduce Location Search + Lookup capabilities - #589
Conversation
…ing/ to generally represent any physical location (to be referenced by Location capability).
There was a problem hiding this comment.
Overall, the shape and proposal make sense—ty for scaffolding this. A few design questions I'd like to work through...
1. Should common be the service, or the namespace containing a Location service?
The capability names are scoped to Location:
dev.ucp.common.location.search
dev.ucp.common.location.lookup
The profile examples, however, advertise the transport as:
{
"services": {
"dev.ucp.common": [{ "...": "..." }]
}
}I agree with dev.ucp.common.* as the namespace for cross-vertical primitives. However, I'm not sure about making dev.ucp.common one catch-all service: every future shared capability would then share an endpoint, transport schema, etc.
The alternative split is:
namespace: dev.ucp.common.*
service: dev.ucp.common.location
capabilities: dev.ucp.common.location.search
dev.ucp.common.location.lookup
Do we think a catch-all .common service is preferred to scoped services?
2. Is geofence_radius sufficient to model the domain?
The current geo object combines a physical point with a circular geofence_radius, while geofence_point asks whether a requested point falls inside that circle. This covers a useful simple case, but in my experience service areas are often irregular polygons, disjoint regions, postal-code unions, etc.
Do we need to model (thin, hopefully) RFC 7946-based service-area shape supporting Polygon / MultiPolygon?
3. Can we adopt a Schema.org-inspired shape instead of inventing a bespoke one?
The latest changes make the custom model deterministic by defining 24:00, precedence between is_closed / is_24_hours / intervals, and a two-entry convention for overnight hours. That is an improvement, but it also highlights how much new protocol syntax and interpretation logic we are defining.
Schema.org's OpeningHoursSpecification already provides the core semantics we need:
dayOfWeek,opens, andcloses;- multiple entries for split shifts;
closes < opensmeans the interval spans the next day;- no
opensmeans closed; validFrom/validThroughcover date-specific exceptions.
A Schema.org-inspired UCP shape could look like:
{
"timezone": "America/New_York",
"hours": [
{ "day_of_week": "monday", "opens": "09:00", "closes": "17:00" },
{ "day_of_week": "monday", "opens": "18:00", "closes": "22:00" },
{ "day_of_week": "friday", "opens": "22:00", "closes": "02:00" }
],
"exception_hours": [
{
"valid_from": "2026-12-24",
"valid_through": "2026-12-24",
"opens": "09:00",
"closes": "14:00"
},
{
"valid_from": "2026-12-25",
"valid_through": "2026-12-25"
}
]
}The contract is compact: each entry describes one open interval; repeated entries for the same day represent split shifts; a closes value earlier than opens spans midnight; and a date-bounded exception without opens means closed for that period. timezone supplies the interpretation context for local times. The example adapts Schema.org's semantics to UCP's snake_case naming and HH:MM convention rather than copying its JSON-LD representation verbatim.
Is there a concrete requirement that OpeningHoursSpecification cannot model? If not, I would align to it and remove the custom closed/24-hour flags, precedence rules, and overnight splitting convention.
4. Is offerings.inventory.quantity an inventory-disclosure query?
The store-finder use case—"which nearby location can fulfill the item I need?"—is valuable. A hard minimum quantity filter, however, lets a caller repeatedly probe thresholds and approximate a store's stock level even when the response never returns a count.
I suggest framing this as Buyer demand and coarse availability, not inventory disclosure:
- reuse UCP's existing availability semantics rather than parallel inventory concept;
- treat requested quantity as intent that a Business may coarsen;
- change language to not imply that exact on-hand counts are returned;
5. What does offerings mean? 😅
Modelled request can filter on:
{
"offerings": {
"amenities": ["..."],
"inventory": [{ "id": "..." }]
}
}The standardized Location entity, however, exposes neither amenities nor offerings. Although the filter contract implies that each returned Location satisfies the requested values, a Platform cannot render the matched facts, inspect additional amenities, or present structured item availability from the response without relying on vendor-defined fields. There is also a category question: amenities are relatively static Location characteristics, while item availability is dynamic and tied to another Catalog or menu identity. Grouping both under offerings may be convenient structurally, but does not necessarily make them one semantic concept.
My inclination would be to put static amenities on Location, model dynamic availability separately using the existing UCP availability vocabulary, and flatten the filter unless offerings gains a clearer cross-vertical contract.
6. What amenity vocabulary is interoperable across Businesses and verticals?
An open string is the right wire type for UCP, but the current proposal defines no well-known values. Should UCP publish a small open vocabulary for interoperable matching, and which initial values are important enough to standardize?
7. What are the capability-specific security and privacy requirements?
Location handles Buyer location hints, enumerates physical sites, describes service coverage, and can be used to probe per-location item availability. The generic Signals reference and current privacy note do not cover those capability-specific risks.
I think the specification needs explicit Security and Privacy Considerations covering:
- coarse-by-default Buyer location and progressive disclosure;
- purpose limitation and retention of location inputs;
- store/site enumeration and rate limiting;
- inventory and availability probing;
- disclosure of internal, private, or non-buyer-visible locations;
- disclosure of precise service-area geometry.
8. What is the bounded Location projection for Catalog and Checkout?
This PR points Checkout’s pickup destination directly at the rich common Location entity. That couples Checkout to the complete Location discovery model: geo, hours, service area, amenities, and every future Location extension would automatically accrete into the transactional response. I don't think that's right.
In Catalog Fulfillment (#507), we deliberately introduced a thin Location projection: a stable location ID plus method description. This avoided embedding an N-store matrix in product results and deferred richer Location facts to a separately negotiated capability. That boundary still makes sense, but I think there is a middle ground: split Location into a bounded base and an extended discovery entity.
Base Location
id
name
address?
│
└── allOf → Extended Location
geo
hours
exception_hours
timezone
amenities
service_area
Catalog and Checkout would use the bounded, buyer-renderable base. Location Search/Lookup would return the extended entity when the service is negotiated. Platform requests would select a Location by stable id rather than asserting Business-owned name/address facts. This preserves the bounded Catalog model from #507, keeps Catalog and Checkout independently renderable, and prevents the full Location discovery model from accreting into every Checkout response.
Location search intentionally permits Business-defined filters, but the schema relied on JSON Schema's implicit open-object default while the prose named additionalProperties as the extension mechanism. Declare the extension point explicitly so schema readers and generated documentation can distinguish intentional extensibility from omission. Strict resolution remains a caller-selected closed-world override.
Location documentation inherited Catalog-specific descriptions, rendering contexts, and a severity policy that Location never defined. It also documented a singular REST path and a filter name that do not exist in the binding/schema. Use Location-specific llms.txt descriptions and render scopes, remove the unsupported severity claim, and align the visible endpoint and filter names with their canonical definitions.
|
+1 on scoping the service to The setup. Suppose "services": {
"dev.ucp.common": [{
"version": "2026-04-08",
"transport": "rest",
"schema": ".../services/common/rest.openapi.json",
"endpoint": "https://business.example.com/ucp"
}]
}One endpoint, one transport document, and one version covering store lookup, review retrieval, and identity linking. That seems to make three fairly ordinary deployments unexpressible:
The scoped form expresses all three directly, including the case where two domains happen to share a host: "services": {
"dev.ucp.common.location": [{
"version": "2026-04-08", "transport": "rest",
"schema": ".../services/common/location/rest.openapi.json",
"endpoint": "https://stores.business.example.com/ucp"
}],
"dev.ucp.common.reviews": [{
"version": "2026-01-11", "transport": "rest",
"schema": ".../services/common/reviews/rest.openapi.json",
"endpoint": "https://ucp.reviews-vendor.example.com/business-123"
}],
"dev.ucp.common.identity": [{
"version": "2026-04-08", "transport": "rest",
"schema": ".../services/common/identity/rest.openapi.json",
"endpoint": "https://business.example.com/ucp"
}]
}The asymmetry I think settles it: scoped services can emulate the catch-all, but the catch-all cannot emulate scoped. A business serving everything from one gateway just points every scoped entry at the same Would it be worth writing the rule down explicitly, something like: a service is the unit of deployment, not the unit of naming? A new service is warranted whenever a domain could plausibly be operated by a different party, on different infrastructure, or on a different release cadence than its siblings. I like that framing because it explains the existing corpus rather than contradicting it. |
For suggestion 2. above could we just drop the circle instead of upgrading it to polygons?
That matters because publishing it as a circle is currently self-contradictory. Proposal: drop
Why this looks better than adding |
…how business should handle contextual hints fallback.
|
Thanks @maximenajim for the feedback! Responses added and requesting for another passthrough. |
|
I completed another pass. The proposal is looking good. I added a few more follow-up comments. |
igrigorik
left a comment
There was a problem hiding this comment.
Thanks Jing — we're headed in the right direction. Lots of good feedback already, I'll focus on a couple of big questions/flags I'd like us to align and iterate on...
Let's reuse the existing Catalog and Shopping contracts
Location should inherit the established search and identifier-resolution semantics wherever they apply. Unqualified serves(target) should mean that any currently available method can serve the target. inventory[].id should remain opaque and Business-resolved, while its status predicate reuses Shopping's availability vocabulary. We should explicitly define omitted and conflicting inventory-status predicates. One forward-compatibility caveat: serves is currently closed and probably shouldn't be; adding a method qualifier later would require a schema/version change unless we preserve that extension point now.
Q: should amenities be a reverse-domain-name keyed map?
We currently model each amenity as a string, which is simple if the contract is only “this Location has this amenity.” Did we consider—or do we want—to model amenities as a reverse-domain-name keyed map instead?
I'd like to stress-test whether we'll eventually want properties attached to an amenity, such as a subtype, restrictions, availability, or other metadata. If amenities are deliberately Boolean membership identifiers, the current string array is fine. If we expect them to become extensible assertions, an RNDS map would give us namespace ownership and room for additional properties without a later array-to-map wire break.
Remaining hours schema and semantic issues together
The latest updates fixed timezone enforcement and some earlier exception-hour inconsistencies, but the current contract still has unresolved issues:
open_nowandopen_atmay both be supplied, with implementation-defined precedence or OR behavior;exception_hoursuses00:00–00:00for full-day closure, leaving no unambiguous 24-hour-open representation and no defined equal-time behavior for regular hours;- exclusive
throughdiffers from Schema.org's inclusivevalidThroughwithout an explicit conversion rule; - regular versus exception precedence, overlapping exceptions, split and overnight intervals, and closure behavior are not defined as one evaluation model;
- the renamed UCP fields do not yet have a mechanical mapping to the corresponding Schema.org vocabulary.
Building on Maxime's existing thread, I think these should be resolved as one atomic schema, prose, and examples change rather than adjusting the pieces independently—potentially as a standalone stacked PR.
Separate the existing Fulfillment destination debt from the Location capability
This is a big one, because it points to existing spec debt. Maxime's oneOf comment exposes an existing Fulfillment problem: the shipping and Business Location destination branches overlap, the request projection does not reduce Location selection to a stable ID reference, and the surrounding contract does not guarantee that a discovered Location is accepted by the applicable Fulfillment method.
Replacing retail_location.json with location_base.json does not by itself solve those issues, and it also changes an existing published path. I recommend handling this as a separately reviewed breaking Fulfillment refactor—discriminator, bounded response projection, ID-based request selection, identity guarantee, and compatibility plan—that #589 can then build on.
Destination authorship is a function of the enclosing method's type:
shipping destinations are Platform-authored; pickup destinations are
Business-authored (the Platform selects one, never writes one); and
extension-defined method types specify their own destination shape and
writability.
- Destination `type` is required in responses, optional in requests.
When a request destination omits it, the method's type implies the
shape: untyped destinations under `shipping` validate as Shipping
Destinations via new method-level conditioning (the conditioning of
destinations[] on method.type the original schema lacked). A Platform
MAY include `type` to disambiguate a reference when more than one
kind can appear under a method (e.g. an id-only destination
referencing a Business-managed mailing address vs an identity
provider's customer address book).
- Removed the writable business-location destination (`{type, id}`
request stub). `selected_destination_id` is the sole
location-selection channel; it accepts any stable, Business-scoped
Location ID the Business recognizes for the method, including IDs not
yet enumerated in destinations[] (the #589 handoff). The Business
returns the typed Location Summary in its response.
- Updated prose, field docs, and request-direction examples accordingly.
Validated: ucp-schema lint, validate_examples (296 pass),
test_validate_examples (50 pass), strict spec build, check_links.
…ly in requests A fulfillment method's type selects the shape of its entire subtree, destinations included: a shipping method has shipping-address destinations, a pickup method has business-location destinations, and extension-defined method types define their own. Polymorphism is resolved at the parent, so request destinations need no per-object discriminator. - fulfillment_method branches per method type; the generic fulfillment_destination union is no longer referenced by schemas (kept for response documentation). - Destination type is required in responses, optional in requests. - destinations under pickup is response-only (ucp_request omit inside the pickup branch): under strict resolution the Platform cannot write business locations. selected_destination_id is the sole selection channel and accepts any Business-scoped Location ID the Business recognizes for the method, including IDs not yet enumerated (#589 handoff). - dependentRequired: a request that writes destinations[] must carry the method's type. - Removed explicit additionalProperties:true from fulfillment_method (behavior-neutral in open validation; lets strict sealing work). - Existing request wire shapes are unchanged; responses gain the required type field. Assisted-By: devx/296664b9-53b6-409a-989a-ace9d3348247
* fix!: discriminate destinations at the method level; type response-only in requests A fulfillment method's type selects the shape of its entire subtree, destinations included: a shipping method has shipping-address destinations, a pickup method has business-location destinations, and extension-defined method types define their own. Polymorphism is resolved at the parent, so request destinations need no per-object discriminator. - fulfillment_method branches per method type; the generic fulfillment_destination union is no longer referenced by schemas (kept for response documentation). - Destination type is required in responses, optional in requests. - destinations under pickup is response-only (ucp_request omit inside the pickup branch): under strict resolution the Platform cannot write business locations. selected_destination_id is the sole selection channel and accepts any Business-scoped Location ID the Business recognizes for the method, including IDs not yet enumerated (#589 handoff). - dependentRequired: a request that writes destinations[] must carry the method's type. - Removed explicit additionalProperties:true from fulfillment_method (behavior-neutral in open validation; lets strict sealing work). - Existing request wire shapes are unchanged; responses gain the required type field. Assisted-By: devx/296664b9-53b6-409a-989a-ace9d3348247 * clarify directional destination typing Fulfillment responses always self-describe with a required destination type, while Platform requests follow the enclosing method's authorship contract. Clarify that untyped destinations under the well-known shipping method default to Shipping Destination, pickup destinations are Business-authored and selected through selected_destination_id, and other method types define their own request shape and Platform writability. Remove the unsupported suggestion that an alternate destination type can be selected under core shipping. An ID-only saved or provider-held address remains a Shipping Destination; provider provenance or additional fields require a negotiated extension contract. --------- Co-authored-by: Ilya Grigorik <ilya@grigorik.com>
* define deterministic operating hours The current Location PR introduces weekly and exceptional operating hours, but leaves several wire and evaluation semantics ambiguous. In particular, closures rely on an artificial midnight interval, `open_now` depends on an implicit server clock, exception date bounds are unclear, and the specification does not define timezone, overnight, DST, overlap, or precedence behavior. Close those gaps with a UCP-native schedule model informed by Schema.org's OpeningHoursSpecification: https://schema.org/OpeningHoursSpecification Schema.org is design input only. UCP owns the field names, values, and evaluation rules defined here. Make weekly intervals explicit and reusable: "hours": [ { "day": "tuesday", "opens": "09:00", "closes": "12:00" }, { "day": "tuesday", "opens": "13:00", "closes": "21:00" } ] Rename `open` and `close` to `opens` and `closes`, and define `day` as a stable UCP weekday identifier rather than localized display text. Multiple entries for one day represent split shifts, and an interval whose closing time is earlier than its opening time continues into the next local date. Refactor the shared time interval schema so `opens` and `closes` are an optional but inseparable pair. Weekly hours require both fields, while exception hours may omit both to represent a full closure. Reject the ambiguous `00:00` to `00:00` pair and reserve `00:00` to `23:59` as the full-local-day sentinel. Replace the previous exception shape: { "from": "2026-11-26", "through": "2026-11-27", "label": "Thanksgiving", "open": "00:00", "close": "00:00" } with inclusive local-date bounds and an actual closure representation: { "title": "Thanksgiving", "valid_from": "2026-11-26", "valid_through": "2026-11-26" } Rename `from`, `through`, and `label` to `valid_from`, `valid_through`, and `title`. Treat `title` as optional presentation metadata that does not affect schedule evaluation. Allow timed exceptions with paired `opens` and `closes`, including multiple entries with identical bounds for split shifts. Define every returned schedule in the Location's Business-owned IANA timezone. Require `timezone` whenever regular or exception hours are present, and keep the canonical schedule independent of the requesting Platform or Buyer's timezone. Specify deterministic evaluation: - convert an exact instant into each Location's local date, weekday, and time - use half-open timed intervals, except for the reserved full-day sentinel - let overnight intervals carry into the following local date - replace regular hours with exception hours at local midnight - treat omitted weekdays as having no interval starting that day - treat absent schedules as unknown rather than closed - evaluate DST gaps and folds pointwise without shifting nonexistent times - reject equal time pairs and intersecting non-identical exception ranges as Business conformance errors where JSON Schema cannot express the constraint Remove the redundant `open_now` filter. It makes results depend on an implicit processing clock and creates undefined precedence when combined with `open_at`. Require one caller-supplied RFC 3339 instant instead: "filters": { "hours": { "open_at": "2026-05-18T17:00:00Z" } } Require `open_at` to include `Z` or a numeric offset. The offset identifies the instant only; the Business still evaluates that instant using each candidate Location's authoritative IANA timezone. Keep the nested hours filter open so extensions can add qualifiers without changing the standard predicate. Move complete Search and Lookup examples into the transport-neutral capability documents. Cover hours with serviceability and amenities, inventory with distance, split shifts, full closures, and partial Lookup success there. Reduce REST and MCP examples to equivalent binding envelopes that link to the same canonical payload examples. This keeps both transports on equal footing, avoids duplicating domain semantics, and prevents one binding's examples from becoming more complete or authoritative than the other. Preserve MCP's required `meta["ucp-agent"].profile` contract while separating protocol metadata from the Location request. This is a breaking correction to the Location PR's draft wire shape: - `open` becomes `opens` - `close` becomes `closes` - `from` becomes `valid_from` - `through` becomes `valid_through` - `label` becomes `title` - `open_now` is removed - full closures omit both time fields instead of using `00:00` to `00:00` * s/weekday/day of week * clarify operating-hours semantics Define `open_at` as the caller-selected instant relevant to the request, such as an expected arrival or pickup time. This avoids framing it as a request for the Business's receipt-time notion of "now": normal request latency does not change the question, and the Business evaluates the supplied instant against each Location's schedule. Describe operating hours more directly as local dates and clock times interpreted using the Location's IANA timezone. Clarify that temporary closures retain the regular `hours` schedule and override it with a date-bounded `exception_hours` entry that omits `opens` and `closes`. Mirror omitted-schedule semantics in the Location schema for implementers who read generated references: - an omitted day has no regular interval beginning that day - an interval from the preceding day may still carry into it - omission of the entire `hours` property means the schedule is unknown Make `time_interval` genuinely reusable by limiting it to generic `HH:MM` opening and closing fields. Location-specific recurrence and timezone interpretation remain with the containing daily, exception, and Location schemas. Remove the schema check that rejected only `00:00`–`00:00`. The actual authoring rule rejects every pair where `opens` equals `closes`, but standard JSON Schema cannot compare sibling values; enforcing one special case would misleadingly imply that other equal pairs are valid. Continue enforcing paired field presence and time formatting mechanically, while keeping unequal times as a normative Business conformance requirement and requiring Platforms not to infer openness from invalid schedule data. * define authority for hours filtering The TC discussion converged on keeping one `open_at` filter, but left open whether both the Platform and Business could apply timing tolerance when interpreting immediate intent. After further consideration, assign that flexibility to one side only. The Platform owns the interpretation of Buyer intent and selects the instant to query. It may use its current time, choose an expected arrival, pickup, or order-acceptance time, and round or adjust that choice to the granularity appropriate to the interaction. Once encoded, however, `open_at` identifies one specific RFC 3339 instant. Require the Business to evaluate that instant exactly as supplied using each Location's authoritative timezone. It must not round, shift, substitute request receipt time, or otherwise reinterpret the value. Allowing both parties to apply independent tolerance would make the evaluated question unknowable and could produce different matches for identical requests near an opening or closing boundary. Apply normal positive-match filter semantics: return a Location only when the Business can establish that it is open at `open_at`. Missing, invalid, out-of-range, or otherwise unusable schedule data is a non-match rather than a reason to guess or adjust the requested instant. Clarify that the numeric offset in `open_at` identifies the queried instant, not the Location's timezone. The Business converts that instant using the Location's authoritative IANA timezone before evaluating its local schedule. State closing-boundary behavior concretely: a `10:00`–`17:00` interval is open immediately before `17:00` and closed at `17:00`. This avoids ambiguity over whether `HH:MM` values represent exact boundaries or minute-sized buckets. Keep exception payloads useful for planning without accumulating stale history. Businesses should remove entries once they cannot affect any current or future instant and publish known future exceptions through the horizon for which their schedule is authoritative. Remove the request-language localization recommendation for exception `title`. The field remains optional presentation metadata, but this capability does not define a localization guarantee for it.
…eing treated as a net new schema type.
|
Thanks @maximenajim and @igrigorik for this latest round of feedback! Let's reuse the existing Catalog and Shopping contractsQ: should amenities be a reverse-domain-name keyed map?PTAL 8178018. I agree with the reverse-DNS approach, especially given that the same amenity name may actually mean very different things depending on the industry is in (namespace clashes). Some notable examples:
However, I think we are too early to think about modelling for a more structured/complex representation. The starting use case in my mind right now is to understand whether the amenity is available/offered at a location and nothing more granular. If the need arrives in the future, we can explore defining the schema to model these amenities (and a potential non-breaking way to add it into our existing modelling can be to add a dedicated Remaining hours schema and semantic issues togetherThanks for helping to draft #687, now landed and should address this concern here! Separate the existing Fulfillment destination debt from the Location capabilityBoth have pointed this out and I agree with the gap here. My proposal is in alignment to the recommendation of tl;dr:
|
maximenajim
left a comment
There was a problem hiding this comment.
The design is materially stronger than the initial draft. Thanks for the updates.
|
Three small consistency items at 1fdd791. The Location section in fulfillment.md now renders common/types/location.json while fulfillment_destination.json still validates shipping_destination or retail_location, so the published Fulfillment doc and the wire schema describe different entities; reverting that leftover hunk also avoids a textual conflict with #688, which edits the same lines. Smaller: index.md still describes geofence_point coverage checks that no schema or binding defines (the shipped filter is serves), and serves accepts both {} and point plus address together, which search.md leaves undefined; anyOf requiring point or address plus one precedence sentence would close it. |
* fix!: require destination type Fulfillment originally paired a closed shipping/pickup method enum with an untagged oneOf between Shipping Destination and Retail Location. Humans could infer the intended shape from the enclosing method, but the schema did not condition destinations[] on method.type. Every destination was validated against both branches. The branches are not structurally disjoint. Shipping Destination is open and requires only id in responses, so every Retail Location also matches it. Strict oneOf validation rejects intended pickup destinations. Request resolution has the same defect: it omits the stable Location id and requires Business-owned name/address fields, which the open Shipping branch also accepts. PR #507 opened the method-type vocabulary but left this inherited destination debt unchanged. PR #589 exposes it again and adds a second boundary problem: using the full Location Search/Lookup entity in Checkout would couple Fulfillment to a separately negotiated capability's schema and lifecycle. Checkout and Location Lookup need different projections: - Checkout request: stable type-plus-id Location reference - Checkout response: bounded id/name/address rendering summary - Location Search/Lookup: richer discovery entity with geo, hours, amenities, service areas, and future Location fields Platforms must be able to negotiate and render Checkout without supporting or invoking Location Lookup. This change: - Introduces a bounded Location Summary with stable Business-scoped id, Buyer-facing name, and optional address. PR #589 can compose its richer Location entity on top without leaking discovery fields into Checkout. - Replaces structural destination inference with a required, open type discriminator. Well-known values are shipping_address and business_location; negotiated extensions may define additional values. - Preserves flat Platform-owned shipping-address fields. Shipping requests keep destination id optional; Business responses continue assigning the id. - Changes Business Location requests to type-plus-id references. The Business owns the Location name/address and returns those facts in the response summary. - Keeps Catalog's scalar Location id and selected_destination_id semantics. Location recognition is method-scoped and does not reserve inventory or guarantee eligibility; the Business revalidates current terms through normal Fulfillment responses and messages. - Generalizes the active pickup destination from Retail Location to Business Location Destination. Retail stores remain supported as Business Locations, alongside other Business-scoped places such as lockers and partner pickup points. - Updates all destination examples and documents the new authority, identity, and extension boundaries. BREAKING CHANGE: every active Fulfillment Destination now requires type. Shipping-address producers must add type: shipping_address. Retail Location destinations migrate to type: business_location; requests send the stable Location id instead of name/address. * fix!: discriminate destinations at the method level (#689) * fix!: discriminate destinations at the method level; type response-only in requests A fulfillment method's type selects the shape of its entire subtree, destinations included: a shipping method has shipping-address destinations, a pickup method has business-location destinations, and extension-defined method types define their own. Polymorphism is resolved at the parent, so request destinations need no per-object discriminator. - fulfillment_method branches per method type; the generic fulfillment_destination union is no longer referenced by schemas (kept for response documentation). - Destination type is required in responses, optional in requests. - destinations under pickup is response-only (ucp_request omit inside the pickup branch): under strict resolution the Platform cannot write business locations. selected_destination_id is the sole selection channel and accepts any Business-scoped Location ID the Business recognizes for the method, including IDs not yet enumerated (#589 handoff). - dependentRequired: a request that writes destinations[] must carry the method's type. - Removed explicit additionalProperties:true from fulfillment_method (behavior-neutral in open validation; lets strict sealing work). - Existing request wire shapes are unchanged; responses gain the required type field. Assisted-By: devx/296664b9-53b6-409a-989a-ace9d3348247 * clarify directional destination typing Fulfillment responses always self-describe with a required destination type, while Platform requests follow the enclosing method's authorship contract. Clarify that untyped destinations under the well-known shipping method default to Shipping Destination, pickup destinations are Business-authored and selected through selected_destination_id, and other method types define their own request shape and Platform writability. Remove the unsupported suggestion that an alternate destination type can be selected under core shipping. An ID-only saved or provider-held address remains a Shipping Destination; provider provenance or additional fields require a negotiated extension contract. --------- Co-authored-by: Ilya Grigorik <ilya@grigorik.com> * fix eof * restore generic destination response Moving destinations entirely into method-specific conditionals orphaned the Fulfillment Destination schema and removed destinations from generic method models and generated documentation. It also left extension-defined method responses without the shared type/id destination contract. Restore response-only destinations on the base Fulfillment Method using the generic Fulfillment Destination schema. Known method branches refine that base: shipping re-enables Platform-writable request destinations, while pickup destinations remain response-only. This keeps extension-defined method responses typed, restores destinations to generated models and field tables, and makes the generic schema normative instead of documentation-only. * style(schema): remove redundant destination optionality text * clarify default destination selection The Curbside review exposed two gaps in how the specification describes the existing destination contract. The schema already gives every method a safe default—destinations are Business-authored and response-only, the Platform selects by ID, and returned destinations self-describe through type and id—but the prose described non-Shipping/Pickup methods as undefined. It also gave IDs learned through Catalog a stronger continuity guarantee than IDs learned from Location or an earlier Checkout. Clarify that default and make accepted selection source-agnostic. When the Business accepts a non-null selected_destination_id, it returns the same value with exactly one matching typed destination, revalidates availability and terms, and never silently substitutes another destination. If an Update cannot be accepted, the Business leaves the current Checkout unchanged and returns a recoverable Message identifying the attempted selection. Remove the stale mental-model line that placed Curbside beneath Pickup; Curbside remains a peer method and already inherits the default contract without a new schema branch. No schema change is required because fulfillment_method and fulfillment_destination already express this behavior. Separately, we *have* replicated well-known values across too many places, but I'm reserving that for a separate and followup commit. * clarify destination authorship Describe method and destination types with human-facing examples, state when the Platform writes destination inputs, and use the protocol's "omitted" terminology. Explain shipping destinations as either inline addresses or saved-address references resolvable by the Business or a trusted Credential Provider, and clarify the Checkout request and response flow. --------- Co-authored-by: Lee Richmond <richmolj@gmail.com>
igrigorik
left a comment
There was a problem hiding this comment.
Great work and progress! Reviewing again with fresh eyes, a few things...
Let's reconcile Location with the landed fulfillment contract
#688 landed after this branch was last updated, so #589 should consume that contract rather than retain its temporary parallel representation:
- compose rich
common/types/location.jsonfromcommon/types/location_summary.jsonand removelocation_base.json; - take #688's Fulfillment schemas and documentation, including
location_destination,location_summary, and the source-agnostic selection lifecycle; - retain the existing
ucp_shared_requestannotations instead of removing them as unrelated cleanup; - clarify that a Location ID returned by Search or Lookup may be submitted as
selected_destination_idon an applicable method, while the method'stypeidentifies the fulfillment mode and the Business revalidates the selection.
This leaves #688 as the single authority for the Checkout projection and selection lifecycle.
make serves a first-class Location Search query?
Spicy: I think we're abusing/overusing filters. serves asks the core retrieval question “which Locations serve this target?” Filters should be used to narrow candidate Locations by properties such as hours, amenities, and inventory. I think we should hoist serves out of filters.geo and making it a sibling of filters.
{
"serves": {
"point": { "latitude": 37.422, "longitude": -122.084 }
},
"filters": {
"hours": { "open_at": "2026-08-18T18:00:00Z" },
"amenities": ["dev.ucp.amenity.shopping.curbside_pickup"]
},
"pagination": { "limit": 10 }
}serves is independently sufficient to make this request meaningful.
As a non-blocking design question: do we want the base Location capability to offer free-text query at all, or should natural-language retrieval be extension-defined? Either answer can coexist with serves; I do not think that question needs to block fixing the structured query shape.
If method-specific narrowing is introduced later, it belongs in filters as a sibling attribute filter. When present with serves, it narrows the method set used to evaluate serviceability; without it, serves means that at least one currently available method can serve the target.
As a related bug/issue: current schema instead accepts an empty target, an empty address, or contradictory targets such as a Seattle point paired with a New York address:
{
"serves": {
"point": { "latitude": 47.6062, "longitude": -122.3321 },
"address": { "address_country": "US", "postal_code": "10001" }
}
}One Business could evaluate the point, another the address, and a third require both. We should define what we want the behavior to be. I believe the intended contract here is ~oneOf but we also don't want to use that to close extensibility. If so, one idea to explore is to model it as single-entry map: minProperties: 1 and maxProperties: 1, with point and address properties. This enforces exactly one target without a closed oneOf or an additional target wrapper.
Clarify Location browse and pagination
What happens when serves is omitted or is an empty object? This is the "browse" flow. My expectation is that the Business returns up to the requested or default page size, ordered by its relevance policy after applying filters. It may use request context, observed signals such as coarse IP-derived locality, or a Business-defined default set. These hints do not establish a distance boundary; a Platform supplies an explicit distance query when it needs one.
Location Search already supports pagination.limit, opaque request/response cursors, and has_next_page, so browse is paginated rather than a fixed or unbounded “all locations” result.
Availability vocabulary
inventory[].availability_status currently documents the positive subset in_stock, backorder, and preorder, while Shopping Availability also documents out_of_stock and discontinued. Is Location intentionally defining a subset, or should Shopping identifiers use the same well-known values and meanings as Shopping Availability?
Amenity vocabulary and documentation pass
The reverse-DNS string array answers the wire-shape question. Lets add a small normative table defining the initial dev.ucp.amenity.* identifiers and fix location.json's bare examples such as free_wifi and parking, which do not satisfy the schema. Platforms should tolerate unknown amenity identifiers in responses; an unrecognized identifier used as a request filter must not be silently treated as satisfied.
Also, a few stale references to fix:
geofence_point→serves;offerings.inventory→filters.inventory;- Lookup's “offerings/inventory” →
inventory; - bare amenity examples → valid reverse-DNS identifiers.
Punt on Lookup aliases?
Lookup says multiple identifiers may resolve to one Location, returns that Location once, and then says clients correlate solely by matching returned location.id to requested IDs. Those rules cannot all hold when an input is an alias.
Catalog needs multiple identifier forms and therefore returns required inputs[] correlation metadata. Does Location need that complexity now? The smaller v1 contract is to accept canonical, stable Business Location.id values only, deduplicate exact duplicate strings, and require every returned location.id to equal a requested ID. If aliases or secondary identifiers are required, Location needs lookup-only correlation equivalent to Catalog's inputs[]; returning duplicate Location objects does not solve the mapping.
Description
A mirror copy of #545 that supersedes it.
Defines standard interfaces for discovering, searching, and retrieving physical locations (e.g., retail stores, restaurants, warehouses, lodging properties).
It introduces two new capabilities under the
dev.ucp.commonnamespace (consistent with Shopping's Catalog capability design):dev.ucp.common.location.search): Discovery-focused endpoint for natural language query, geographic, and offerings-based filters.dev.ucp.common.location.lookup): Resolution-focused endpoints supporting single & batch lookups.Some key commerce flows it will be able to unlock:
nearby that support customer pickup and checking their operating hours & inventory availability
before selection.
or local service provider) has delivery coverage for a buyer's address.
Category (Required)
ucp-schematool (resolver, linter, validator). (Requires Maintainer approval)Related Issues
This is related to RFC #375's section 10.
Checklist
!for breaking changes).Screenshots / Logs (if applicable)