diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index 861f02bbf..61b24632a 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -125,3 +125,20 @@ worktree yaml yml zapatillas +jwks +keyid +reauth +redeemables +reprepare +sandboxing +fbclid +gclid +ttclid +Accor +Kogan +Petbarn +VTEX +geofence +geofencing +wifi +optionalities diff --git a/docs/specification/cart-rest.md b/docs/specification/cart-rest.md index c6c700a2e..c68abc95d 100644 --- a/docs/specification/cart-rest.md +++ b/docs/specification/cart-rest.md @@ -472,7 +472,7 @@ All REST endpoints **MUST** be served over HTTPS with minimum TLS version 1.3. The following headers are defined for the HTTP binding and apply to all operations unless otherwise noted. -{{ header_fields('create_cart', 'rest.openapi.json') }} +{{ header_fields('create_cart', 'shopping/rest.openapi.json') }} ### Specific Header Requirements diff --git a/docs/specification/catalog/rest.md b/docs/specification/catalog/rest.md index cd5b2ca31..4561d1c56 100644 --- a/docs/specification/catalog/rest.md +++ b/docs/specification/catalog/rest.md @@ -71,7 +71,7 @@ Businesses advertise REST transport availability through their UCP profile at Maps to the [Catalog Search](search.md) capability. -{{ method_fields('search_catalog', 'rest.openapi.json', 'catalog/rest') }} +{{ method_fields('search_catalog', 'shopping/rest.openapi.json', 'catalog/rest') }} #### Example @@ -201,7 +201,7 @@ for supported identifiers, resolution behavior, and client correlation requireme The request body contains an array of identifiers and optional context that applies to all lookups in the batch. -{{ method_fields('lookup_catalog', 'rest.openapi.json', 'catalog/rest') }} +{{ method_fields('lookup_catalog', 'shopping/rest.openapi.json', 'catalog/rest') }} #### Example: Batch Lookup with Context @@ -353,7 +353,7 @@ messages indicating which identifiers were not found. Maps to the [Catalog Lookup](lookup.md#get-product-get_product) capability. Returns a singular `product` object (not an array) for full product detail page rendering. -{{ method_fields('get_product', 'rest.openapi.json', 'catalog/rest') }} +{{ method_fields('get_product', 'shopping/rest.openapi.json', 'catalog/rest') }} #### Example: With Option Selection diff --git a/docs/specification/checkout-rest.md b/docs/specification/checkout-rest.md index dd32d254d..d8a7aa7f5 100644 --- a/docs/specification/checkout-rest.md +++ b/docs/specification/checkout-rest.md @@ -1280,7 +1280,7 @@ place to set these expectations via `messages`. The following headers are defined for the HTTP binding and apply to all operations unless otherwise noted. -{{ header_fields('create_checkout', 'rest.openapi.json') }} +{{ header_fields('create_checkout', 'shopping/rest.openapi.json') }} ### Specific Header Requirements diff --git a/docs/specification/checkout.md b/docs/specification/checkout.md index 6e0d5e15e..c6819e16c 100644 --- a/docs/specification/checkout.md +++ b/docs/specification/checkout.md @@ -630,7 +630,7 @@ should accept an additional `cart_id` field for cart-to-checkout conversion. See [Cart → Cart-to-Checkout Conversion](cart.md#cart-to-checkout-conversion) for the field contract. -{{ method_fields('create_checkout', 'rest.openapi.json', 'checkout') }} +{{ method_fields('create_checkout', 'shopping/rest.openapi.json', 'checkout') }} ### Get Checkout @@ -643,7 +643,7 @@ checkout. The platform will honor the TTL provided by the business via `expires_at` at the time of checkout session creation. -{{ method_fields('get_checkout', 'rest.openapi.json', 'checkout') }} +{{ method_fields('get_checkout', 'shopping/rest.openapi.json', 'checkout') }} ### Update Checkout @@ -652,7 +652,7 @@ The platform is **REQUIRED** to send the entire checkout resource containing any data updates to write-only data fields. The resource provided in the request will replace the existing checkout session state on the business side. -{{ method_fields('update_checkout', 'rest.openapi.json', 'checkout') }} +{{ method_fields('update_checkout', 'shopping/rest.openapi.json', 'checkout') }} ### Complete Checkout @@ -668,7 +668,7 @@ to construct the order representation (i.e. information like `line_items`, After this call, other details will be updated through subsequent events as the order, and its associated items, moves through the supply chain. -{{ method_fields('complete_checkout', 'rest.openapi.json', 'checkout') }} +{{ method_fields('complete_checkout', 'shopping/rest.openapi.json', 'checkout') }} ### Cancel Checkout @@ -678,7 +678,7 @@ already canceled or completed), then businesses **SHOULD** send back an error indicating the operation is not allowed. Any checkout session with a status that is not equal to `completed` or `canceled` **SHOULD** be cancelable. -{{ method_fields('cancel_checkout', 'rest.openapi.json', 'checkout') }} +{{ method_fields('cancel_checkout', 'shopping/rest.openapi.json', 'checkout') }} ## Transport Bindings diff --git a/docs/specification/fulfillment.md b/docs/specification/fulfillment.md index aa4973c31..08a7a6419 100644 --- a/docs/specification/fulfillment.md +++ b/docs/specification/fulfillment.md @@ -81,9 +81,9 @@ method. {{ schema_fields('types/shipping_destination_resp', 'fulfillment') }} -#### Retail Location +#### Location -{{ schema_fields('types/retail_location_resp', 'fulfillment') }} +{{ schema_fields('types/location_resp', 'fulfillment') }} #### Fulfillment Group diff --git a/docs/specification/location/index.md b/docs/specification/location/index.md new file mode 100644 index 000000000..ff1a4e50c --- /dev/null +++ b/docs/specification/location/index.md @@ -0,0 +1,148 @@ + + +# Location Capability + +The Location capability allows platforms to discover, search, and retrieve physical locations +(such as retail stores, restaurants, brand lockers) from businesses. + +This is vertical-agnostic and enables key commerce flows such as: + +* **Local Pickup Discovery**: Finding locations like retail stores or restaurant branches + nearby that support customer pickup and checking their operating hours & inventory availability + before selection. +* **Fulfillment Area Verification**: Checking if a specific location (e.g., utility depot, restaurant, + or local service provider) has delivery coverage for a buyer's address. + +## Capabilities + +| Capability | Description | +| :--- | :--- | +| [`dev.ucp.common.location.search`](search.md) | Search for locations using natural language queries and filters (hours, offerings, geofencing). | +| [`dev.ucp.common.location.lookup`](lookup.md) | Retrieve full details for one or more locations by identifier. | + +## Key Concepts + +* **Location**: A physical entity that can be found on a map. Defined by a display name, + address, operating hours, and **geographic context** (geographic coordinates and an + optional circular **geofence service radius** for delivery/service area checks). +* **Offerings**: Features, capabilities, and inventory provided by the location. + This is split into two distinct concepts to ensure tooling compatibility and semantic clarity: + * **Amenities**: Static features, services, or capabilities of the location + (e.g., `free_wifi`, `parking`, `outdoor_seating`, `curbside_pickup`). + * **Inventory**: Dynamic availability of goods (e.g., retail products or restaurant dishes). +* **Geofencing**: Locations can define a `geofence_radius` around their coordinates. + This is used to determine if a location can serve a specific user (e.g., delivery area check). + Clients can perform proximity searches (`distance` filter) or coverage checks (`geofence` filter) using the filter. +* **Operating Hours**: Weekly schedules (`hours`) and date-specific overrides + (`exception_hours` - e.g., holidays, temporary closures) associated with a timezone. + +### Relationship to Other Capabilities + +The Location capability provides the foundation for localized commerce by integrating tightly +other capabilities (like Catalog, Cart, and Checkout in Shopping): + +1. **Stable Identifiers**: Location search/lookup operations return stable, + business-scoped `location.id` values. These IDs are referenced further in other requests & responses + (e.g., associating product variants to specific locations in Catalog filters, passed directly + in `selected_destination_id` to indicate pickup fulfillment mode). +2. **Inventory-Based Store Finder**: Platforms can use Location Search with the `offerings.inventory` filter + to locate nearby stores that have a specific item available, bridging the gap between online catalog + browsing and physical store visits. +3. **Provisional vs. Authoritative Boundaries**: + * *Discovery Phase (Provisional)*: Location responses based on operating hours, real-time inventory + availability, and amenities offerings represent the business's *current terms* at the + time of query. They are **provisional signals** (despite most, like hours & amenities, remain stable + overtime) and are not binding commitments. + * *Checkout Phase (Authoritative)*: Final transaction terms that depend on a location (e.g., pickup) + **MUST** be negotiated and finalized authoritatively. Discovery signals **SHOULD NOT** be cached + or reused across sessions without re-validation. + +## Shared Entities + +### Context + +User location and market context for the operations. All fields are optional +hints for relevance and localization. Platforms **MAY** geo-detect context from +request headers. + +Context signals are provisional—not authoritative data. Businesses **SHOULD** use +these values when verified inputs (e.g., coordinates as part of the request filter) +are absent, and **MAY** ignore or down-rank them if inconsistent with +higher-confidence signals (authenticated account, risk detection). + +{{ schema_fields('types/context', 'location') }} + +### Signals + +Environment data provided by the platform to support authorization +and abuse prevention. Signal values **MUST NOT** be buyer-asserted claims. See +[Signals](../overview.md#signals) for details and privacy requirements. + +{{ schema_fields('types/signals', 'location') }} + +## Messages and Error Handling + +All location responses include an optional `messages` array that allows businesses +to provide context about errors, warnings, or informational notices. + +### Message Types + +Messages communicate business outcomes and provide context: + +| Type | When to Use | Example Codes | +| :--- | :--- | :--- | +| `error` | Business-level errors | `no_service_coverage` (geographic coordinates based filter) | +| `warning` | Important conditions affecting purchase | `permanently_closed`, `temporary_closure` | +| `info` | Additional context without issues | `not_found`, `holiday_hours_active` | + +**Note**: Most catalog errors use `severity: "recoverable"` - agents +handle them programmatically (retry, inform user, show alternatives). + +#### Message (Error) + +{{ schema_fields('types/message_error', 'catalog') }} + +#### Message (Warning) + +{{ schema_fields('types/message_warning', 'catalog') }} + +#### Message (Info) + +{{ schema_fields('types/message_info', 'catalog') }} + +### Common Scenarios + +#### Empty Search + +When search finds no matches, return an empty array without messages. + + +```json +{ + "ucp": {...}, + "locations": [] +} +``` + +This is not an error - the query was valid but returned no results. + +## Transport Bindings + +The capabilities above are bound to specific transport protocols: + +* [REST Binding](rest.md): RESTful API mapping. +* [MCP Binding](mcp.md): Model Context Protocol mapping via JSON-RPC. diff --git a/docs/specification/location/lookup.md b/docs/specification/location/lookup.md new file mode 100644 index 000000000..59e4a9deb --- /dev/null +++ b/docs/specification/location/lookup.md @@ -0,0 +1,76 @@ + + +# Location Lookup Capability + +* **Capability Name:** `dev.ucp.common.location.lookup` + +Retrieves physical locations by their unique identifiers. +Supports full-detail batch retrieval of multiple locations to provide optionalities +or retrieval of a single location (useful for a dedicated location detail page). + +## Operation + +| Operation | Description | +| :--------------------- | :-------------------------------------------- | +| **Lookup Location(s)** | Retrieve single or multiple locations by ID. | + +## Supported Identifiers + +The `ids` parameter accepts an array of identifiers. Implementations **MUST** +support lookup by the business's stable location ID. + +Duplicate identifiers in the request **MUST** be deduplicated by the server. +When multiple identifiers resolve to the same physical location, +it **MUST** be returned only once in the response. + +### Client Correlation + +The response does not guarantee order. Clients correlate returned locations +simply by matching the returned `id` field against their requested `ids`. + +### Batch Size + +Implementations **SHOULD** accept at least 10 identifiers per request. +Implementations **MAY** enforce a maximum batch size and **MUST** reject +requests exceeding their limit with an appropriate error (HTTP 400 +`request_too_large` for REST, JSON-RPC `-32602` for MCP). + +### Filters + +Optional `filters` (hours, offerings/inventory, geo) are accepted +to narrow down the returned locations. +Filters use the same schema and AND semantics as [Search Filters](search.md#search-filters). + +Filters apply **after** identifier resolution. For example, if a client requests +`["loc_downtown", "loc_uptown"]` with a filter of `hours.open_now: true`: + +1. The server first resolves both identifiers to their respective locations. +2. The server then evaluates the `open_now` filter against each resolved location. +3. If `loc_uptown` is currently closed, it is excluded, and only `loc_downtown` is returned. + +### Request + +{{ extension_schema_fields('location_lookup.json#/$defs/lookup_request', 'location') }} + +### Response + +{{ extension_schema_fields('location_lookup.json#/$defs/lookup_response', 'location') }} + +## Transport Bindings + +* [REST Binding](rest.md#post-locationslookup): `POST /locations/lookup` +* [MCP Binding](mcp.md#lookup_locations): `lookup_locations` tool diff --git a/docs/specification/location/mcp.md b/docs/specification/location/mcp.md new file mode 100644 index 000000000..aa946940d --- /dev/null +++ b/docs/specification/location/mcp.md @@ -0,0 +1,329 @@ + + +# Location - MCP Binding + +This document specifies the Model Context Protocol (MCP) binding for the [Location Capability](index.md). + +## Protocol Fundamentals + +### Discovery + +Businesses advertise MCP transport availability for the Common service and Location capabilities through their UCP profile at `/.well-known/ucp`. + + +```json +{ + "ucp": { + "version": "{{ ucp_version }}", + "services": { + "dev.ucp.common": [ + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview", + "transport": "mcp", + "schema": "https://ucp.dev/{{ ucp_version }}/services/common/mcp.openrpc.json", + "endpoint": "https://business.example.com/ucp/mcp" + } + ] + }, + "capabilities": { + "dev.ucp.common.location.search": [{ + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/location/search", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/common/location_search.json" + }], + "dev.ucp.common.location.lookup": [{ + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/location/lookup", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/common/location_lookup.json" + }] + }, + "payment_handlers": {} + } +} +``` + +### Request Metadata + +MCP clients **MUST** include a `meta` object in every request containing +protocol metadata: + + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "search_locations", + "arguments": { + "meta": { + "ucp-agent": { + "profile": "https://platform.example/profiles/v2026-01/agent.json" + } + }, + "location": { + "query": "grocery store open now", + "filters": { + "hours": { + "open_now": true + } + } + } + } + } +} +``` + +The `meta["ucp-agent"]` field is **required** on all requests to enable +version compatibility checking and capability negotiation. + +## Tools + +| Tool | Capability | Description | +| :--- | :--- | :--- | +| `search_locations` | [Search](search.md) | Search for locations using text, coordinates, and filters. | +| `lookup_locations` | [Lookup](lookup.md) | Batch lookup one or multiple location(s) by ID. | + +### `search_locations` + +Maps to the [Location Search](search.md) capability. + +#### Request Arguments + +{{ extension_schema_fields('location_search.json#/$defs/search_request', 'location/mcp') }} + +#### Response Schema + +{{ extension_schema_fields('location_search.json#/$defs/search_response', 'location/mcp') }} + +#### Example + +=== "Request" + + + ```json + { + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "search_locations", + "arguments": { + "meta": { + "ucp-agent": { + "profile": "https://platform.example/profiles/v2026-01/agent.json" + } + }, + "location": { + "query": "grocery store near me", + "context": { + "address_country": "US", + "address_region": "CA", + "postal_code": "94043" + }, + "filters": { + "hours": { + "open_now": true + }, + "offerings": { + "amenities": ["curbside_pickup"] + }, + "geo": { + "geofence_point": { + "latitude": 37.422, + "longitude": -122.084 + } + } + } + } + } + } + } + ``` + +=== "Response" + + + ```json + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "structuredContent": { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.common.location.search": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "locations": [ + { + "id": "loc_valley_grocers", + "name": "Valley Grocers", + "address": { + "street_address": "789 Maple Ave", + "address_locality": "Mountain View", + "address_region": "CA", + "address_country": "US", + "postal_code": "94043" + }, + "geo": { + "latitude": 37.420, + "longitude": -122.080, + "geofence_radius": 10000 + }, + "timezone": "America/Los_Angeles" + } + ] + } + } + } + ``` + +### `lookup_locations` + +Maps to the [Location Lookup](lookup.md) capability. + +#### Request Arguments + +{{ extension_schema_fields('location_lookup.json#/$defs/lookup_request', 'location/mcp') }} + +#### Response Schema + +{{ extension_schema_fields('location_lookup.json#/$defs/lookup_response', 'location/mcp') }} + +#### Example + +=== "Request" + + + ```json + { + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": { + "name": "lookup_locations", + "arguments": { + "meta": { + "ucp-agent": { + "profile": "https://platform.example/profiles/v2026-01/agent.json" + } + }, + "location": { + "ids": ["loc_downtown", "loc_uptown"] + } + } + } + } + ``` + +=== "Response" + + + ```json + { + "jsonrpc": "2.0", + "id": 2, + "result": { + "structuredContent": { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.common.location.lookup": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "locations": [ + { + "id": "loc_downtown", + "name": "Downtown Store", + "address": { + "street_address": "100 Broadway", + "address_locality": "New York", + "address_region": "NY", + "address_country": "US", + "postal_code": "10005" + }, + "geo": { + "latitude": 40.707, + "longitude": -74.011, + "geofence_radius": 2000 + }, + "timezone": "America/New_York" + } + ], + "messages": [ + { + "type": "info", + "code": "not_found", + "content": "Unable to find the location associated with loc_uptown" + } + ] + } + } + } + ``` + +## Error Handling + +UCP uses a two-layer error model separating transport-level errors from business outcomes. + +### Transport Errors + +Transport-level failures (authentication, rate limiting, invalid parameters) that prevent request processing are returned as JSON-RPC `error`. See the [Core Specification](../overview.md#error-codes) for details. + +### Business Outcomes + +All application-level outcomes return a successful JSON-RPC result with the UCP envelope and optional `messages` array. See [Location Overview](index.md#messages-and-error-handling) for message semantics. + +## Entities + +### Location {: #location-entity } + +{{ schema_fields('types/location', 'location/mcp') }} + +### Location Filter {: #location-filter-schema } + +{{ schema_fields('types/location_filter', 'location/mcp') }} + +### Location Offering Filter {: #location-offering-filter-schema } + +{{ schema_fields('types/location_offering_filter', 'location/mcp') }} + +### Error Response {: #error-response } + +{{ schema_fields('types/error_response', 'location/mcp') }} + +## Conformance + +A conforming MCP transport implementation **MUST**: + +1. Implement JSON-RPC 2.0 protocol correctly. +2. Implement tools for each location capability advertised in the business's UCP profile, per their respective + capability requirements ([Search](search.md), [Lookup](lookup.md)). + Each capability may be adopted independently. +3. Default to business-derived coordinates based on user location hint provided in `context.json` + for proximity (`distance`) filters when explicit coordinates are omitted by the platform in the request. +4. Return a successful JSON-RPC result for lookup requests; unknown identifiers result in fewer or no locations + returned (**MAY** include informational `not_found` messages in the `messages` array). +5. Validate tool inputs against UCP schemas. +6. Return `-32602` (Invalid params) for requests exceeding batch size limits. diff --git a/docs/specification/location/rest.md b/docs/specification/location/rest.md new file mode 100644 index 000000000..9797cd445 --- /dev/null +++ b/docs/specification/location/rest.md @@ -0,0 +1,532 @@ + + +# Location - REST Binding + +This document specifies the HTTP/REST binding for the [Location Capability](index.md). + +## Protocol Fundamentals + +### Discovery + +Businesses advertise REST transport availability for the Common service and +Location capabilities through their UCP profile at `/.well-known/ucp`. + + +```json +{ + "ucp": { + "version": "{{ ucp_version }}", + "services": { + "dev.ucp.common": [ + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview", + "transport": "rest", + "schema": "https://ucp.dev/{{ ucp_version }}/services/common/rest.openapi.json", + "endpoint": "https://business.example.com/ucp" + } + ] + }, + "capabilities": { + "dev.ucp.common.location.search": [{ + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/location/search", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/common/location_search.json" + }], + "dev.ucp.common.location.lookup": [{ + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/location/lookup", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/common/location_lookup.json" + }] + }, + "payment_handlers": {} + } +} +``` + +## Endpoints + +| Endpoint | Method | Capability | Description | +| :--- | :--- | :--- | :--- | +| `/locations/search` | POST | [Search](search.md) | Search for physical locations. | +| `/locations/lookup` | POST | [Lookup](lookup.md) | Lookup single or multiple location(s) by known ID. | + +### `POST /locations/search` + +Maps to the [Location Search](search.md) capability. + +{{ method_fields('search_locations', 'common/rest.openapi.json', 'location/rest') }} + +#### Example: Search for Grocery Stores with Local Delivery Coverage (Geofencing) + +=== "Request" + + + ```json + POST /locations/search HTTP/1.1 + Host: business.example.com + Content-Type: application/json + Request-Id: 8ef9b0c2-78d1-4e4b-91c2-3e2ef0d3ab9f + UCP-Agent: profile="https://platform.example/profiles/v2026-01/agent.json" + + { + "query": "grocery store near me", + "context": { + "address_country": "US", + "address_region": "CA", + "postal_code": "94043" + }, + "filters": { + "hours": { + "open_now": true + }, + "offerings": { + "amenities": ["curbside_pickup"] + }, + "geo": { + "geofence_point": { + "latitude": 37.422, + "longitude": -122.084 + } + } + } + } + ``` + +=== "Response" + + + ```json + HTTP/1.1 200 OK + Content-Type: application/json + Content-Digest: sha-256=:yG9a8bC7...: + + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.common.location.search": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "locations": [ + { + "id": "loc_valley_grocers", + "name": "Valley Grocers", + "address": { + "street_address": "789 Maple Ave", + "address_locality": "Mountain View", + "address_region": "CA", + "address_country": "US", + "postal_code": "94043" + }, + "geo": { + "latitude": 37.420, + "longitude": -122.080, + "geofence_radius": 10000 + }, + "timezone": "America/Los_Angeles" + } + ] + } + ``` + +#### Example: Search for Electronics Stores with Phone In-stock (Store Finder) + +=== "Request" + + + ```json + POST /locations/search HTTP/1.1 + Host: business.example.com + Content-Type: application/json + Request-Id: 9ef9b0c2-78d1-4e4b-91c2-3e2ef0d3ab9f + UCP-Agent: profile="https://platform.example/profiles/v2026-01/agent.json" + + { + "context": { + "address_country": "US" + }, + "filters": { + "hours": { + "open_now": true + }, + "offerings": { + "inventory": [ + { + "id": "item_id_phone_15_pro", + "quantity": 1 + } + ] + }, + "geo": { + "distance": { + "center": { + "latitude": 40.707, + "longitude": -74.011 + }, + "max_distance": 10000 + } + } + } + } + ``` + +=== "Response" + + + ```json + HTTP/1.1 200 OK + Content-Type: application/json + + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.common.location.search": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "locations": [ + { + "id": "loc_downtown_electronics", + "name": "Downtown Electronics", + "address": { + "street_address": "100 Broadway", + "address_locality": "New York", + "address_region": "NY", + "address_country": "US", + "postal_code": "10005" + }, + "geo": { + "latitude": 40.709, + "longitude": -74.008, + "geofence_radius": 2000 + }, + "timezone": "America/New_York" + } + ] + } + ``` + +### `POST /locations/lookup` + +Maps to the [Location Lookup](lookup.md) capability. + +{{ method_fields('lookup_locations', 'common/rest.openapi.json', 'location/rest') }} + +#### Example: Simple Lookup + +=== "Request" + + + ```json + POST /locations/lookup HTTP/1.1 + Host: business.example.com + Content-Type: application/json + Request-Id: 2c9b0c2a-18d1-4e4b-91c2-3e2ef0d3ab9f + UCP-Agent: profile="https://platform.example/profiles/v2026-01/agent.json" + + { + "ids": ["loc_downtown", "loc_uptown"] + } + ``` + +=== "Response" + + + ```json + HTTP/1.1 200 OK + Content-Type: application/json + + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.common.location.lookup": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "locations": [ + { + "id": "loc_downtown", + "name": "Downtown Store", + "address": { + "street_address": "100 Broadway", + "address_locality": "New York", + "address_region": "NY", + "address_country": "US", + "postal_code": "10005" + }, + "geo": { + "latitude": 40.707, + "longitude": -74.011, + "geofence_radius": 2000 + }, + "timezone": "America/New_York", + "hours": [ + { + "day": "monday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "tuesday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "wednesday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "thursday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "friday", + "intervals": [{"open": "09:00", "close": "22:00"}] + }, + { + "day": "saturday", + "intervals": [{"open": "10:00", "close": "20:00"}] + }, + { + "day": "sunday", + "is_closed": true + } + ], + "exception_hours": [ + { + "date": "2026-11-26", + "label": "Thanksgiving", + "is_closed": true + } + ] + }, + { + "id": "loc_uptown", + "name": "Uptown Boutique", + "address": { + "street_address": "2000 Madison Ave", + "address_locality": "New York", + "address_region": "NY", + "address_country": "US", + "postal_code": "10035" + }, + "geo": { + "latitude": 40.790, + "longitude": -73.950, + "geofence_radius": 1000 + }, + "timezone": "America/New_York", + "hours": [ + { + "day": "monday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "tuesday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "wednesday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "thursday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "friday", + "intervals": [{"open": "09:00", "close": "22:00"}] + }, + { + "day": "saturday", + "is_closed": true + }, + { + "day": "sunday", + "is_closed": true + } + ], + "exception_hours": [ + { + "date": "2026-11-26", + "label": "Thanksgiving", + "is_closed": true + } + ] + } + ] + } + ``` + +#### Example: Partial Success (Some Locations Not Found) + +=== "Request" + + + ```json + POST /locations/lookup HTTP/1.1 + Host: business.example.com + Content-Type: application/json + Request-Id: 2c9b0c2a-18d1-4e4b-91c2-3e2ef0d3ab9f + UCP-Agent: profile="https://platform.example/profiles/v2026-01/agent.json" + + { + "ids": ["loc_downtown", "loc_invalid_id"] + } + ``` + +=== "Response" + + + ```json + HTTP/1.1 200 OK + Content-Type: application/json + + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.common.location.lookup": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "locations": [ + { + "id": "loc_downtown", + "name": "Downtown Store", + "address": { + "street_address": "100 Broadway", + "address_locality": "New York", + "address_region": "NY", + "address_country": "US", + "postal_code": "10005" + }, + "geo": { + "latitude": 40.707, + "longitude": -74.011, + "geofence_radius": 2000 + }, + "timezone": "America/New_York", + "hours": [ + { + "day": "monday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "tuesday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "wednesday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "thursday", + "intervals": [{"open": "09:00", "close": "21:00"}] + }, + { + "day": "friday", + "intervals": [{"open": "09:00", "close": "22:00"}] + }, + { + "day": "saturday", + "intervals": [{"open": "10:00", "close": "20:00"}] + }, + { + "day": "sunday", + "is_closed": true + } + ], + "exception_hours": [ + { + "date": "2026-11-26", + "label": "Thanksgiving", + "is_closed": true + } + ] + } + ], + "messages": [ + { + "type": "info", + "code": "not_found", + "content": "Unable to find the location associated with loc_invalid_id." + } + ] + } + ``` + +## Error Handling + +UCP uses a two-layer error model separating transport-level errors from business outcomes. + +### Transport Errors + +Use HTTP status codes for protocol-level issues that prevent request processing: + +| Status | Meaning | +| :--- | :--- | +| 400 | Bad Request - Malformed JSON or missing required parameters | +| 401 | Unauthorized - Missing or invalid authentication | +| 429 | Too Many Requests - Rate limited | +| 500 | Internal Server Error | + +### Business Outcomes + +All application-level outcomes return HTTP 200 with the UCP envelope and optional `messages` array. See [Location Overview](index.md#messages-and-error-handling) for message semantics. + +## Entities + +### UCP Response Catalog (Envelope) {: #ucp-response-catalog-schema } + +{{ extension_schema_fields('ucp.json#/$defs/response_catalog_schema', 'location/rest') }} + +### Location {: #location-entity } + +{{ schema_fields('types/location', 'location/rest') }} + +### Location Filter {: #location-filter-schema } + +{{ schema_fields('types/location_filter', 'location/rest') }} + +### Location Offering Filter {: #location-offering-filter-schema } + +{{ schema_fields('types/location_offering_filter', 'location/rest') }} + +### Error Response {: #error-response } + +{{ schema_fields('types/error_response', 'location/rest') }} + +## Conformance + +A conforming REST transport implementation **MUST**: + +1. Implement endpoints for each location capability advertised in the business's UCP profile, + per their respective capability requirements ([Search](search.md), [Lookup](lookup.md)). + Each capability **MAY** be adopted independently. +2. Default to business-derived coordinates based on user location hint provided in `context.json` + for proximity (`distance`) filters when explicit coordinates are omitted by the platform in the request. +3. Support cursor-based pagination with a default limit of 10 for search results. +4. Return HTTP 200 for lookup requests; unknown identifiers result in fewer or no locations + returned (**MAY** include informational `not_found` messages). +5. Return HTTP 400 with `request_too_large` error for requests exceeding batch size limits. diff --git a/docs/specification/location/search.md b/docs/specification/location/search.md new file mode 100644 index 000000000..75286f45f --- /dev/null +++ b/docs/specification/location/search.md @@ -0,0 +1,123 @@ + + +# Location Search Capability + +* **Capability Name:** `dev.ucp.common.location.search` + +Performs a search for physical locations (e.g., retail stores, restaurants, +warehouses). Supports natural language queries, geographic proximity (distance) +searches, and structured filtering by operating hours and offerings such as +amenities and inventory availability. + +## Operation + +| Operation | Description | +| :--- | :--- | +| **Search Locations** | Search for locations using query text, context, and filters. | + +### Request + +{{ extension_schema_fields('location_search.json#/$defs/search_request', 'location') }} + +### Response + +{{ extension_schema_fields('location_search.json#/$defs/search_response', 'location') }} + +## Search Inputs + +A valid search request **MUST** include at least one of: a `query` string +or one or more `filters`. When `query` is omitted, the request represents +a browse operation — the business returns locations matching the provided +filters without text-relevance ranking. + +Implementations **MUST** validate that incoming requests contain at least one +recognized input and **SHOULD** reject empty or invalid requests with an +appropriate error. Implementations define and enforce their own rules for +input presence and content — for example, requiring `query`, rejecting +empty `query` strings, or accepting filter-only requests. + +## Search Filters + +Location filters allow narrowing results based on specific criteria. +Standard filters are defined as below; businesses **MAY** support additional +custom filters via `additionalProperties`. + +{{ schema_fields('types/location_filter', 'location') }} + +### Hours-Based Filter + +Filters locations based on their operating hours: + +* `open_now`: A quick boolean filter to find locations currently open. +* `open_at`: An RFC 3339 date-time string to find locations open at a + specific future time (e.g., planning a visit or ordering ahead). + The business resolves this against the location's local time and timezone. + +### Offerings-Based Filter + +Separates static location characteristics from dynamic availability: + +* **`amenities`** (Array of Strings): Static features or services of the + location (e.g., `free_wifi`, `parking`, `outdoor_seating`, `curbside_pickup`, `vegetarian`). + All specified amenities **MUST** be supported by the location (AND semantic). +* **`inventory`** (Array of Objects): Real-time availability of items/goods at + the location. Some industry specific use cases include: + * *Shopping*: Checking stock levels for specific products or variants. + * *Food Ordering*: Checking availability of specific dishes or menu items. + Each inventory filter requires an `id` (product/dish ID) and can optionally specify + a `type` to help multi-industry business with backend routing and a + minimum `quantity`. + +### Geographic & Geofencing Filter + +Supports two distinct, industry-agnostic spatial search models: + +* **`distance` (Proximity Search)**: Filters for locations within a `max_distance` + (in RFC 7035 distance units = meters) of a `center` point. + +> **Privacy Integration**: If `center` is omitted, the server **MUST** use the +> user's address hint provided in the request `context` (which may be coarse/sanitized) +> to derive the center. + +* **`geofence_point` (Service Area Coverage)**: Filters for locations whose circular + service area (defined by `geofence_radius`) contains the specified point. + +## Pagination + +Cursor-based pagination for list operations. Cursors are opaque strings +that implementations **MAY** encode as stateless keyset tokens. + +### Page Size + +The `limit` parameter is a requested page size, not a guaranteed count. +Implementations **SHOULD** accept a page size of at least 10. When the +requested limit exceeds the implementation's maximum, implementations +**MAY** clamp to their maximum silently — returning fewer results without +error. Clients MUST NOT assume the response size equals the requested limit. + +### Pagination Request + +{{ extension_schema_fields('types/pagination.json#/$defs/request', 'location') }} + +### Pagination Response + +{{ extension_schema_fields('types/pagination.json#/$defs/response', 'location') }} + +## Transport Bindings + +* [REST Binding](rest.md#post-locationssearch): `POST /location/search` +* [MCP Binding](mcp.md#search_locations): `search_locations` tool diff --git a/docs/specification/order-rest.md b/docs/specification/order-rest.md index 73818e32d..f453eda35 100644 --- a/docs/specification/order-rest.md +++ b/docs/specification/order-rest.md @@ -227,7 +227,7 @@ Returns the current-state snapshot of an order. ## HTTP Headers -{{ header_fields('get_order', 'rest.openapi.json') }} +{{ header_fields('get_order', 'shopping/rest.openapi.json') }} ### Specific Header Requirements diff --git a/docs/specification/order.md b/docs/specification/order.md index cdd1b28c1..61bc2c2e9 100644 --- a/docs/specification/order.md +++ b/docs/specification/order.md @@ -433,7 +433,7 @@ See [Message Signatures](signatures.md) for more details. | `Webhook-Timestamp` | Event occurrence timestamp (unix) | | `Webhook-Id` | Unique event identifier | -{{ method_fields('order_event_webhook', 'rest.openapi.json', 'order') }} +{{ method_fields('order_event_webhook', 'shopping/rest.openapi.json', 'order') }} ### Webhook URL Configuration diff --git a/docs/specification/reference.md b/docs/specification/reference.md index d84d3da49..fa55047c1 100644 --- a/docs/specification/reference.md +++ b/docs/specification/reference.md @@ -21,6 +21,8 @@ within the UCP. ## Capability Schemas +{{ auto_generate_schema_reference('.', 'reference', include_extensions=False, base_dir='source/schemas/common') }} + {{ auto_generate_schema_reference('.', 'reference', include_extensions=False) }} ## Type Schemas diff --git a/main.py b/main.py index 2a9684beb..e6969e617 100644 --- a/main.py +++ b/main.py @@ -27,7 +27,7 @@ # --- CONFIGURATION --- # Base directories for schema resolution -OPENAPI_DIR = Path("source/services/shopping") +OPENAPI_DIR = Path("source/services") SCHEMAS_DIR = Path("source/schemas") HANDLERS_GOOGLE_PAY_DIR = Path("source/handlers/google_pay") COMMON_SCHEMAS_DIR = SCHEMAS_DIR / "common" diff --git a/mkdocs.yml b/mkdocs.yml index 8d77486d2..3abae00cc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -74,6 +74,13 @@ nav: - REST: specification/order-rest.md - MCP: specification/order-mcp.md - Identity Linking Capability: specification/identity-linking.md + - Location Capability: + - Overview: specification/location/index.md + - Search: specification/location/search.md + - Lookup: specification/location/lookup.md + - Transports: + - REST: specification/location/rest.md + - MCP: specification/location/mcp.md - Payment Handlers: - Guide: specification/payment-handler-guide.md - Template: specification/payment-handler-template.md @@ -370,6 +377,24 @@ plugins: Model Context Protocol (MCP) transport binding for the Catalog Capability, mapping discovery operations to JSON-RPC tools with metadata validation rules. + Location Capability: + - specification/location/index.md: >- + Core Location Capability, detailing high-level product discovery + models, variant structures, merchant attribution, and real-time + pricing context. + - specification/location/search.md: >- + Product discovery via the Search Location capability, specifying + text queries and filters such as operating hours and offerings. + - specification/location/lookup.md: >- + Direct location retrieval via the Lookup Location capability, + specifying identifiers and additional filter option. + - specification/location/rest.md: >- + HTTP REST transport binding for the Location Capability, + detailing search and lookup endpoints with JSON payload examples. + - specification/location/mcp.md: >- + Model Context Protocol (MCP) transport binding for the Location + Capability, mapping discovery operations to JSON-RPC tools with + metadata validation rules. Other Capabilities: - specification/order.md: >- Post-purchase tracking via the Order Capability, detailing line diff --git a/source/schemas/common/location_lookup.json b/source/schemas/common/location_lookup.json new file mode 100644 index 000000000..14074882e --- /dev/null +++ b/source/schemas/common/location_lookup.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/location_lookup.json", + "name": "dev.ucp.common.location.lookup", + "title": "Location Lookup", + "description": "Location lookup by identifier. Supports batch retrieval (lookup_locations) and single-location detail (get_location).", + "type": "object", + "$defs": { + "lookup_request": { + "type": "object", + "description": "Request body for batch location lookup.", + "required": ["ids"], + "properties": { + "ids": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "description": "Identifiers of the locations to lookup. Implementations MUST support location ID." + }, + "filters": { + "$ref": "types/location_filter.json", + "description": "Filter criteria to narrow returned locations. All specified filters combine with AND logic." + }, + "context": { + "$ref": "types/context.json" + }, + "signals": { + "$ref": "types/signals.json" + } + } + }, + "lookup_response": { + "type": "object", + "required": [ + "ucp", + "locations" + ], + "properties": { + "ucp": { + "$ref": "../ucp.json#/$defs/response_catalog_schema" + }, + "locations": { + "type": "array", + "items": { + "$ref": "types/location.json" + }, + "description": "Locations matching the requested identifiers and filters. May contain fewer locations if some identifiers are not found." + }, + "messages": { + "type": "array", + "items": { + "$ref": "types/message.json" + }, + "description": "Errors, warnings, or informational messages about the requested locations." + } + } + } + } +} diff --git a/source/schemas/common/location_search.json b/source/schemas/common/location_search.json new file mode 100644 index 000000000..c2556e693 --- /dev/null +++ b/source/schemas/common/location_search.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/location_search.json", + "name": "dev.ucp.common.location.search", + "title": "Location Search", + "description": "Location search capability. Supports natural language queries, structured filtering, and pagination.", + "type": "object", + "$defs": { + "search_request": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Free-text search query for natural language location search (e.g., 'restaurants near me that deliver', 'hotels with pool')." + }, + "context": { + "$ref": "types/context.json" + }, + "signals": { + "$ref": "types/signals.json" + }, + "filters": { + "$ref": "types/location_filter.json" + }, + "pagination": { + "$ref": "types/pagination.json#/$defs/request" + } + } + }, + "search_response": { + "type": "object", + "required": [ + "ucp", + "locations" + ], + "properties": { + "ucp": { + "$ref": "../ucp.json#/$defs/response_catalog_schema" + }, + "locations": { + "type": "array", + "items": { + "$ref": "types/location.json" + }, + "description": "Locations matching the search criteria." + }, + "pagination": { + "$ref": "types/pagination.json#/$defs/response" + }, + "messages": { + "type": "array", + "items": { + "$ref": "types/message.json" + }, + "description": "Errors, warnings, or informational messages about the search results." + } + } + } + } +} diff --git a/source/schemas/shopping/types/context.json b/source/schemas/common/types/context.json similarity index 97% rename from source/schemas/shopping/types/context.json rename to source/schemas/common/types/context.json index 9d7f51207..e3443f4e2 100644 --- a/source/schemas/shopping/types/context.json +++ b/source/schemas/common/types/context.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://ucp.dev/schemas/shopping/types/context.json", + "$id": "https://ucp.dev/schemas/common/types/context.json", "title": "Context", "description": "Provisional buyer signals for relevance and localization—not authoritative data. Businesses SHOULD use these values when verified inputs (e.g., shipping address) are absent, and MAY ignore or down-rank them if inconsistent with higher-confidence signals (authenticated account, risk detection) or regulatory constraints (export controls). Eligibility and policy enforcement MUST occur at checkout time using binding transaction data. Context SHOULD be non-identifying and can be disclosed progressively—coarse signals early, finer resolution as the session progresses. Higher-resolution data (shipping address, billing address) supersedes context.", "type": "object", diff --git a/source/schemas/common/types/daily_hour.json b/source/schemas/common/types/daily_hour.json new file mode 100644 index 000000000..e059d7f6a --- /dev/null +++ b/source/schemas/common/types/daily_hour.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/daily_hour.json", + "title": "Daily Hour", + "description": "Operating hours for a specific day of the week.", + "type": "object", + "required": ["day"], + "properties": { + "day": { + "type": "string", + "enum": ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] + }, + "is_closed": { + "type": "boolean", + "description": "If true, the location is closed on this day. When true, is_24_hours and intervals MUST be omitted." + }, + "is_24_hours": { + "type": "boolean", + "description": "If true, open 24 hours on this day. When true, intervals MUST be omitted." + }, + "intervals": { + "type": "array", + "description": "One or more open intervals for this day. Supports split shifts.", + "items": { + "$ref": "time_interval.json" + } + } + } +} diff --git a/source/schemas/common/types/exception_hour.json b/source/schemas/common/types/exception_hour.json new file mode 100644 index 000000000..57e9880e9 --- /dev/null +++ b/source/schemas/common/types/exception_hour.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/exception_hour.json", + "title": "Exception Hour", + "description": "Operating hours for a specific date (e.g., holiday or temporary change).", + "type": "object", + "required": ["date"], + "properties": { + "date": { + "type": "string", + "format": "date", + "description": "An ISO 8601 date." + }, + "label": { + "type": "string", + "description": "Human readable explanation for the exception (e.g., 'Thanksgiving')." + }, + "is_closed": { + "type": "boolean", + "description": "If true, the location is closed on this date. When true, is_24_hours and intervals MUST be omitted." + }, + "is_24_hours": { + "type": "boolean", + "description": "If true, open 24 hours on this date. When true, intervals MUST be omitted." + }, + "intervals": { + "type": "array", + "description": "One or more open intervals for this date.", + "items": { + "$ref": "time_interval.json" + } + } + } +} diff --git a/source/schemas/common/types/geo.json b/source/schemas/common/types/geo.json new file mode 100644 index 000000000..bd9f4dccb --- /dev/null +++ b/source/schemas/common/types/geo.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/geo.json", + "title": "Geo", + "description": "Geographic information.", + "type": "object", + "required": ["latitude", "longitude"], + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90, + "description": "Latitude in decimal degrees." + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180, + "description": "Longitude in decimal degrees." + }, + "geofence_radius": { + "type": "number", + "description": "Geofence radius in RFC 7035 distance unit (meters). Used for proximity detection." + } + } +} diff --git a/source/schemas/common/types/inventory_filter.json b/source/schemas/common/types/inventory_filter.json new file mode 100644 index 000000000..437f535cb --- /dev/null +++ b/source/schemas/common/types/inventory_filter.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/inventory_filter.json", + "title": "Inventory Filter", + "description": "Filter for a specific inventory and its required availability. `id` is dependent on the industry offering the inventory (e.g., product/variant ID from shopping catalog, dish ID from food menu).", + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "string", + "description": "The unique identifier of the item (e.g., product or variant ID in shopping, or dish ID in food ordering)." + }, + "quantity": { + "type": "integer", + "minimum": 1, + "description": "Minimum quantity required to be available at the location." + } + }, + "additionalProperties": true +} diff --git a/source/schemas/common/types/location.json b/source/schemas/common/types/location.json new file mode 100644 index 000000000..24678e3f2 --- /dev/null +++ b/source/schemas/common/types/location.json @@ -0,0 +1,63 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/location.json", + "title": "Location", + "description": "A physical location (e.g., store, restaurant, locker, warehouse).", + "type": "object", + "required": ["id", "name"], + "additionalProperties": true, + "properties": { + "id": { + "type": "string", + "description": "Unique location identifier.", + "ucp_request": { + "transition": { + "from": "omit", + "to": "optional", + "description": "Location ids MAY be specified by platforms in requests." + } + } + }, + "name": { + "type": "string", + "description": "Location display name.", + "ucp_request": { + "transition": { + "from": "required", + "to": "omit", + "description": "Location names should not be specified by platforms." + } + } + }, + "address": { + "$ref": "postal_address.json", + "description": "Physical address of the location." + }, + "geo": { + "$ref": "geo.json", + "description": "Geographic coordinates and geofence for the location.", + "ucp_request": "omit" + }, + "hours": { + "type": "array", + "description": "Regular weekly operating hours. For overnight hours, use two entries across two days (first entry ending with 24:00 and second entry starting 00:00).", + "items": { + "$ref": "daily_hour.json" + }, + "ucp_request": "omit" + }, + "exception_hours": { + "type": "array", + "description": "Exception hours for specific dates (holidays, closures, etc.). For overnight hours, use two entries across two days (first entry ending with 24:00 and second entry starting 00:00).", + "items": { + "$ref": "exception_hour.json" + }, + "ucp_request": "omit" + }, + "timezone": { + "type": "string", + "description": "IANA timezone identifier (e.g., 'America/New_York'). MUST be set when hours or exception_hours are present. Required for correct interpretation.", + "ucp_request": "omit" + } + } +} diff --git a/source/schemas/common/types/location_filter.json b/source/schemas/common/types/location_filter.json new file mode 100644 index 000000000..7055b4324 --- /dev/null +++ b/source/schemas/common/types/location_filter.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/location_filter.json", + "title": "Location Filter", + "description": "Filter criteria to narrow location search/lookup results. All specified filters combine with AND logic.", + "type": "object", + "properties": { + "geo": { + "type": "object", + "description": "Filter locations by geographic proximity or geofence coverage.", + "properties": { + "distance": { + "type": "object", + "description": "Filter locations within a maximum distance from a center point (proximity search).", + "required": ["max_distance"], + "properties": { + "center": { + "$ref": "geo.json", + "description": "The center coordinates. If omitted, the user's coarse location hints in the request context MUST be used to derive the center via forward geocoding strategies." + }, + "max_distance": { + "type": "number", + "minimum": 0, + "description": "Maximum distance in RFC 7035 distance unit (meters)." + } + }, + "additionalProperties": false + }, + "geofence_point": { + "$ref": "geo.json", + "description": "Filter locations whose geofence contains a specific point (e.g., delivery area check)." + } + }, + "additionalProperties": false + }, + "hours": { + "type": "object", + "description": "Filter by operating hours. If both values are specified, behavior is implementation-defined (usually open_at takes precedence or OR logic is enforced).", + "properties": { + "open_now": { + "type": "boolean", + "description": "Only return locations that are currently open." + }, + "open_at": { + "type": "string", + "format": "date-time", + "description": "An RFC 3339 instant. The business converts it to the location's local timezone and evaluates against the known hours." + } + }, + "additionalProperties": false + }, + "offerings": { + "$ref": "location_offering_filter.json" + } + } +} diff --git a/source/schemas/common/types/location_offering_filter.json b/source/schemas/common/types/location_offering_filter.json new file mode 100644 index 000000000..05c62ec00 --- /dev/null +++ b/source/schemas/common/types/location_offering_filter.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/location_offering_filter.json", + "title": "Location Offering Filter", + "description": "Filter criteria for location offerings, separating static amenities/services from dynamic inventory.", + "type": "object", + "properties": { + "amenities": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Filter by static services, amenities, or capabilities of the location (e.g., 'free_wifi', 'wheelchair_accessible', 'parking', 'curbside_pickup'). Matches locations that have ALL the listed amenities (AND logic)." + }, + "inventory": { + "type": "array", + "items": { + "$ref": "inventory_filter.json" + }, + "description": "Filter by real-time availability of inventory (e.g., retail products or restaurant dishes) at the location. Matches locations that have ALL the listed items in stock (AND logic)." + } + }, + "additionalProperties": true +} diff --git a/source/schemas/shopping/types/signals.json b/source/schemas/common/types/signals.json similarity index 93% rename from source/schemas/shopping/types/signals.json rename to source/schemas/common/types/signals.json index 874823290..cc75942d1 100644 --- a/source/schemas/shopping/types/signals.json +++ b/source/schemas/common/types/signals.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://ucp.dev/schemas/shopping/types/signals.json", + "$id": "https://ucp.dev/schemas/common/types/signals.json", "title": "Signals", "description": "Environment data provided by the platform to support authorization and abuse prevention. Values MUST NOT be buyer-asserted claims — platforms provide signals based on direct observation or independently verifiable third-party attestations. All signal keys MUST use reverse-domain naming to ensure provenance and prevent collisions when multiple extensions contribute to the shared namespace.", "type": "object", diff --git a/source/schemas/common/types/time_interval.json b/source/schemas/common/types/time_interval.json new file mode 100644 index 000000000..f9eedd129 --- /dev/null +++ b/source/schemas/common/types/time_interval.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/time_interval.json", + "title": "Time Interval", + "description": "An open time interval with 24-hour HH:MM format. `close` MUST be later than `open`", + "type": "object", + "required": ["open", "close"], + "properties": { + "open": { + "type": "string", + "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$", + "description": "Start time (e.g., '09:00')." + }, + "close": { + "type": "string", + "pattern": "^(([01][0-9]|2[0-3]):[0-5][0-9]|24:00)$", + "description": "End time (e.g., '18:00')." + } + } +} diff --git a/source/schemas/shopping/cart.json b/source/schemas/shopping/cart.json index e7c451727..ad80d7de3 100644 --- a/source/schemas/shopping/cart.json +++ b/source/schemas/shopping/cart.json @@ -62,7 +62,7 @@ } }, "context": { - "$ref": "types/context.json", + "$ref": "../common/types/context.json", "description": "Buyer signals for localization (country, region, postal_code). Merchant uses for pricing, availability, currency. Falls back to geo-IP if omitted.", "ucp_request": { "create": "optional", @@ -70,7 +70,7 @@ } }, "signals": { - "$ref": "types/signals.json", + "$ref": "../common/types/signals.json", "ucp_request": { "create": "optional", "update": "optional" diff --git a/source/schemas/shopping/catalog_lookup.json b/source/schemas/shopping/catalog_lookup.json index 2fe7e657a..08ee0b909 100644 --- a/source/schemas/shopping/catalog_lookup.json +++ b/source/schemas/shopping/catalog_lookup.json @@ -39,10 +39,10 @@ "description": "Filter criteria to narrow returned products and variants. All specified filters combine with AND logic." }, "context": { - "$ref": "types/context.json" + "$ref": "../common/types/context.json" }, "signals": { - "$ref": "types/signals.json" + "$ref": "../common/types/signals.json" }, "attribution": { "$ref": "types/attribution.json" @@ -112,10 +112,10 @@ "description": "Filter criteria to narrow returned variants. All specified filters combine with AND logic." }, "context": { - "$ref": "types/context.json" + "$ref": "../common/types/context.json" }, "signals": { - "$ref": "types/signals.json" + "$ref": "../common/types/signals.json" }, "attribution": { "$ref": "types/attribution.json" diff --git a/source/schemas/shopping/catalog_search.json b/source/schemas/shopping/catalog_search.json index 46bdb2670..ee9753539 100644 --- a/source/schemas/shopping/catalog_search.json +++ b/source/schemas/shopping/catalog_search.json @@ -14,10 +14,10 @@ "description": "Free-text search query." }, "context": { - "$ref": "types/context.json" + "$ref": "../common/types/context.json" }, "signals": { - "$ref": "types/signals.json" + "$ref": "../common/types/signals.json" }, "attribution": { "$ref": "types/attribution.json" diff --git a/source/schemas/shopping/checkout.json b/source/schemas/shopping/checkout.json index 7b5aa69d1..7ae7fe443 100644 --- a/source/schemas/shopping/checkout.json +++ b/source/schemas/shopping/checkout.json @@ -47,7 +47,7 @@ } }, "context": { - "$ref": "types/context.json", + "$ref": "../common/types/context.json", "ucp_request": { "create": "optional", "update": "optional", @@ -55,7 +55,7 @@ } }, "signals": { - "$ref": "types/signals.json", + "$ref": "../common/types/signals.json", "ucp_request": "optional" }, "attribution": { diff --git a/source/schemas/shopping/types/fulfillment_destination.json b/source/schemas/shopping/types/fulfillment_destination.json index cc6708dc9..736bb6e93 100644 --- a/source/schemas/shopping/types/fulfillment_destination.json +++ b/source/schemas/shopping/types/fulfillment_destination.json @@ -10,7 +10,8 @@ "$ref": "shipping_destination.json" }, { - "$ref": "retail_location.json" + "$ref": "../../common/types/location.json", + "description": "A pickup retail location (e.g. store, locker, etc.)." } ] } diff --git a/source/schemas/shopping/types/retail_location.json b/source/schemas/shopping/types/retail_location.json deleted file mode 100644 index 2e9d5d88f..000000000 --- a/source/schemas/shopping/types/retail_location.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://ucp.dev/schemas/shopping/types/retail_location.json", - "title": "Retail Location", - "description": "A pickup location (retail store, locker, etc.).", - "type": "object", - "ucp_shared_request": true, - "required": ["id", "name"], - "additionalProperties": true, - "properties": { - "id": { - "type": "string", - "description": "Unique location identifier.", - "ucp_request": "omit" - }, - "name": { - "type": "string", - "description": "Location name (e.g., store name)." - }, - "address": { - "$ref": "../../common/types/postal_address.json", - "description": "Physical address of the location." - } - } -} diff --git a/source/services/common/mcp.openrpc.json b/source/services/common/mcp.openrpc.json new file mode 100644 index 000000000..4efbde845 --- /dev/null +++ b/source/services/common/mcp.openrpc.json @@ -0,0 +1,97 @@ +{ + "openrpc": "1.3.2", + "info": { + "title": "UCP Common Service", + "description": "Canonical MCP/JSON-RPC interface for UCP Common services, including Location discovery. Schema references are logical pointers - actual payload shape is determined by negotiated capabilities.\n\n**Endpoint Resolution:** This spec defines methods only. The endpoint URL MUST be obtained from the merchant's discovery profile at `/.well-known/ucp` under `services[\"dev.ucp.common\"][transport=mcp].endpoint`. The server entry below is a placeholder for tooling compatibility." + }, + "servers": [ + { + "name": "business", + "url": "{endpoint}", + "description": "Business-provided endpoint from UCP discovery profile", + "variables": { + "endpoint": { + "default": "https://business.example.com/ucp/mcp", + "description": "Obtain from /.well-known/ucp → services[\"dev.ucp.common\"][transport=mcp].endpoint" + } + } + } + ], + "components": { + "schemas": { + "meta": { + "type": "object", + "description": "Request metadata mapping to UCP standard headers.", + "required": ["ucp-agent"], + "additionalProperties": true, + "properties": { + "ucp-agent": { + "type": "object", + "description": "Platform agent identification. Maps to HTTP UCP-Agent header.", + "required": ["profile"], + "properties": { + "profile": { + "type": "string", + "format": "uri", + "description": "URL to the platform's UCP profile document." + } + } + }, + "idempotency-key": { + "type": "string", + "format": "uuid", + "description": "Unique key for retry safety. Maps to HTTP Idempotency-Key header (optional for read-only operations)." + }, + "signature": { + "type": "string", + "description": "Detached JWS signature in format `..` for message integrity." + } + } + } + } + }, + "methods": [ + { + "name": "search_locations", + "summary": "Search for physical locations", + "description": "Search for physical locations (e.g., retail stores, restaurants) using query text, filters, and geo proximity.", + "params": [ + { + "name": "meta", + "required": true, + "schema": {"$ref": "#/components/schemas/meta"} + }, + { + "name": "location", + "required": true, + "schema": {"$ref": "../../schemas/common/location_search.json#/$defs/search_request"} + } + ], + "result": { + "name": "response", + "schema": {"$ref": "../../schemas/common/location_search.json#/$defs/search_response"} + } + }, + { + "name": "lookup_locations", + "summary": "Batch lookup locations by identifier", + "description": "Batch lookup of physical locations by their stable identifiers. Returns matching locations and warnings.", + "params": [ + { + "name": "meta", + "required": true, + "schema": {"$ref": "#/components/schemas/meta"} + }, + { + "name": "location", + "required": true, + "schema": {"$ref": "../../schemas/common/location_lookup.json#/$defs/lookup_request"} + } + ], + "result": { + "name": "response", + "schema": {"$ref": "../../schemas/common/location_lookup.json#/$defs/lookup_response"} + } + } + ] +} diff --git a/source/services/common/rest.openapi.json b/source/services/common/rest.openapi.json new file mode 100644 index 000000000..984eb2ac9 --- /dev/null +++ b/source/services/common/rest.openapi.json @@ -0,0 +1,259 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "UCP Common Service", + "description": "Canonical REST interface for UCP Common services, including Location discovery. Schema references are logical pointers - actual payload shape is determined by negotiated capabilities.\n\n**Endpoint Resolution:** This spec defines operations only. The base URL MUST be obtained from the merchant's discovery profile at `/.well-known/ucp` under `services[\"dev.ucp.common\"][transport=rest].endpoint`. The `{endpoint}` server variable below is a placeholder for tooling compatibility." + }, + "servers": [ + { + "url": "{endpoint}", + "description": "Business-provided endpoint from UCP discovery profile", + "variables": { + "endpoint": { + "default": "https://business.example.com/ucp", + "description": "Obtain from /.well-known/ucp → services[\"dev.ucp.common\"][transport=rest].endpoint" + } + } + } + ], + "paths": { + "/locations/search": { + "post": { + "operationId": "search_locations", + "summary": "Search Locations", + "description": "Search for physical locations (e.g., retail stores, restaurants) using query text, geo proximity, and structured filters (e.g., hours, amenities, inventory).", + "parameters": [ + { "$ref": "#/components/parameters/authorization" }, + { "$ref": "#/components/parameters/x_api_key" }, + { "$ref": "#/components/parameters/signature" }, + { "$ref": "#/components/parameters/signature_input" }, + { "$ref": "#/components/parameters/content_digest" }, + { "$ref": "#/components/parameters/request_id" }, + { "$ref": "#/components/parameters/user_agent" }, + { "$ref": "#/components/parameters/ucp_agent" }, + { "$ref": "#/components/parameters/content_type" }, + { "$ref": "#/components/parameters/accept" }, + { "$ref": "#/components/parameters/accept_language" }, + { "$ref": "#/components/parameters/accept_encoding" } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/location_search_request" } + } + } + }, + "responses": { + "200": { + "description": "Search results", + "headers": { + "Signature": { "$ref": "#/components/headers/signature" }, + "Signature-Input": { "$ref": "#/components/headers/signature_input" }, + "Content-Digest": { "$ref": "#/components/headers/content_digest" } + }, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/location_search_response" } + } + } + } + } + } + }, + "/locations/lookup": { + "post": { + "operationId": "lookup_locations", + "summary": "Batch Lookup Locations", + "description": "Lookup one or more physical locations by their stable identifiers.", + "parameters": [ + { "$ref": "#/components/parameters/authorization" }, + { "$ref": "#/components/parameters/x_api_key" }, + { "$ref": "#/components/parameters/signature" }, + { "$ref": "#/components/parameters/signature_input" }, + { "$ref": "#/components/parameters/content_digest" }, + { "$ref": "#/components/parameters/request_id" }, + { "$ref": "#/components/parameters/user_agent" }, + { "$ref": "#/components/parameters/ucp_agent" }, + { "$ref": "#/components/parameters/content_type" }, + { "$ref": "#/components/parameters/accept" }, + { "$ref": "#/components/parameters/accept_language" }, + { "$ref": "#/components/parameters/accept_encoding" } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/location_lookup_request" } + } + } + }, + "responses": { + "200": { + "description": "Lookup results", + "headers": { + "Signature": { "$ref": "#/components/headers/signature" }, + "Signature-Input": { "$ref": "#/components/headers/signature_input" }, + "Content-Digest": { "$ref": "#/components/headers/content_digest" } + }, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/location_lookup_response" } + } + } + } + } + } + } + }, + "components": { + "parameters": { + "authorization": { + "name": "Authorization", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Contains OAuth token representing platform or user credentials." + }, + "x_api_key": { + "name": "X-API-Key", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Reusable API key allocated to the platform by the business." + }, + "signature": { + "name": "Signature", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "RFC 9421 HTTP Message Signature. Format: `sig1=::`." + }, + "signature_input": { + "name": "Signature-Input", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "RFC 9421 Signature-Input header. Format: `sig1=(\"@method\" \"@path\" ...);created=;keyid=\"\"`." + }, + "content_digest": { + "name": "Content-Digest", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Body digest per RFC 9530. Format: `sha-256=::`." + }, + "request_id": { + "name": "Request-Id", + "in": "header", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Unique UUID for tracing requests across components." + }, + "user_agent": { + "name": "User-Agent", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Identifies the user agent string making the call." + }, + "ucp_agent": { + "name": "UCP-Agent", + "in": "header", + "required": true, + "schema": { + "type": "string" + }, + "description": "Identifies the UCP agent making the call, containing the signer's profile URI. Format: profile=\"https://example.com/.well-known/ucp\"." + }, + "content_type": { + "name": "Content-Type", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Representation Metadata describing the body content." + }, + "accept": { + "name": "Accept", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Content Negotiation, indicating accepted formats." + }, + "accept_language": { + "name": "Accept-Language", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Preferred natural languages for localization." + }, + "accept_encoding": { + "name": "Accept-Encoding", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Supported content-codings (compression)." + } + }, + "headers": { + "signature": { + "required": false, + "schema": { + "type": "string" + }, + "description": "RFC 9421 HTTP Message Signature for response." + }, + "signature_input": { + "required": false, + "schema": { + "type": "string" + }, + "description": "RFC 9421 Signature-Input header for response." + }, + "content_digest": { + "required": false, + "schema": { + "type": "string" + }, + "description": "Body digest per RFC 9530 for response." + } + }, + "schemas": { + "location_search_request": { + "$ref": "../../schemas/common/location_search.json#/$defs/search_request" + }, + "location_search_response": { + "$ref": "../../schemas/common/location_search.json#/$defs/search_response" + }, + "location_lookup_request": { + "$ref": "../../schemas/common/location_lookup.json#/$defs/lookup_request" + }, + "location_lookup_response": { + "$ref": "../../schemas/common/location_lookup.json#/$defs/lookup_response" + } + } + } +}