diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index edf126445..b1f70e899 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -158,3 +158,20 @@ punycode userinfo examplecorp 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 7c71790f2..d9ebb00b7 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..866c91803 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 @@ -514,7 +514,7 @@ with the same identifier. The following headers are defined for the HTTP binding and apply to all operations unless otherwise noted. -{{ header_fields('search_catalog', 'rest.openapi.json') }} +{{ header_fields('search_catalog', 'shopping/rest.openapi.json') }} ### Specific Header Requirements diff --git a/docs/specification/checkout-rest.md b/docs/specification/checkout-rest.md index 653851c97..3b9212eea 100644 --- a/docs/specification/checkout-rest.md +++ b/docs/specification/checkout-rest.md @@ -1294,7 +1294,7 @@ for status and `order` semantics. 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 4222e17bf..930811967 100644 --- a/docs/specification/checkout.md +++ b/docs/specification/checkout.md @@ -790,7 +790,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 @@ -803,7 +803,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 @@ -815,7 +815,7 @@ general replacement rule does not apply during `complete_in_progress` because Update Checkout is not permitted; see [Accepted completion](#accepted-completion) for the frozen operation contract. -{{ method_fields('update_checkout', 'rest.openapi.json', 'checkout') }} +{{ method_fields('update_checkout', 'shopping/rest.openapi.json', 'checkout') }} ### Complete Checkout @@ -856,7 +856,7 @@ to construct the order representation (i.e. information like `line_items`, After the order is placed, other details will be updated through subsequent events as the order, and its associated items, move through the supply chain. -{{ method_fields('complete_checkout', 'rest.openapi.json', 'checkout') }} +{{ method_fields('complete_checkout', 'shopping/rest.openapi.json', 'checkout') }} ### Cancel Checkout @@ -866,7 +866,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 ffb5658e8..2109f90c0 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/glossary.md b/docs/specification/glossary.md index 716e41f7f..6c9bcc8b3 100644 --- a/docs/specification/glossary.md +++ b/docs/specification/glossary.md @@ -40,12 +40,13 @@ acronym in each specification Markdown file spells out the full term (e.g., ## Commerce -| Term | Acronym | Definition | -| :--------------------------- | :------ | :---------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Business** | - | The entity selling goods or services. In UCP, they act as the **Merchant of Record (MoR)**, retaining financial liability and ownership of the order. | -| **Merchant of Record** | MoR | The legal entity responsible for the sale, including financial liability and order ownership. | -| **Payment Service Provider** | PSP | The financial infrastructure provider that processes payments, authorizations, and settlements on behalf of the business. | -| **Platform** | - | The consumer-facing surface (AI agent, app, website) acting on behalf of the user to discover businesses and facilitate commerce. | +| Term | Acronym | Definition | +| :--------------------------- | :------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Business** | - | The entity selling goods or services. In UCP, they act as the **Merchant of Record (MoR)**, retaining financial liability and ownership of the order. | +| **Merchant of Record** | MoR | The legal entity responsible for the sale, including financial liability and order ownership. | +| **Payment Service Provider** | PSP | The financial infrastructure provider that processes payments, authorizations, and settlements on behalf of the business. | +| **Platform** | - | The consumer-facing surface (AI agent, app, website) acting on behalf of the user to discover businesses and facilitate commerce. | +| **Location** | - | A physical entity that can be found on a map. Identified by a stable, business-scoped string (id) and referenced across UCP for user contextualization and location discovery.| ## Payments diff --git a/docs/specification/location/index.md b/docs/specification/location/index.md new file mode 100644 index 000000000..b04fb0ddd --- /dev/null +++ b/docs/specification/location/index.md @@ -0,0 +1,159 @@ + + +# 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). +* **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 & Service Area**: Locations implicitly carry a geofence boundary 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_point` 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` | + +#### Message (Error) + +{{ schema_fields('types/message_error', 'location') }} + +#### Message (Warning) + +{{ schema_fields('types/message_warning', 'location') }} + +#### Message (Info) + +{{ schema_fields('types/message_info', 'location') }} + +### 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. + +## Security & Privacy Considerations + +1. **Coarse-by-default**: Platforms **SHOULD** default to sending coarse location hints (e.g., postal code or rounded coordinates) during the discovery phase. + Precise locations/coordinates **SHOULD** only be shared when the user explicitly consents or selects a specific location. +2. **Inventory Probing Mitigation**: Businesses **SHOULD** implement rate-limiting on search requests, especially if containing inventory availability filters, + to prevent scraping & aggressive numeration of the entire directory. +3. **Private/Dark Locations**: Businesses **MUST** filter out internal-only or non-user-accessible locations (e.g., dark kitchens, fulfillment-only hubs) + from search results. +4. **Physical Address Spoofing (Integrity)**: While location discovery is read-only, tampering with physical addresses in responses (e.g., through MITM attacks) + poses a physical safety/fraud risk. Platforms **SHOULD** verify signatures on location payloads before rendering them to users. +5. **Data Retention & Logging Sanitization**: Businesses **MUST NOT** persist precise location inputs beyond the lifecycle of the request, unless explicit user + consent is collected. Server logs should sanitize coordinate inputs by truncating decimal places (e.g., to 2 decimal places, ~1km accuracy) to prevent + accidental storage of precise user history. diff --git a/docs/specification/location/lookup.md b/docs/specification/location/lookup.md new file mode 100644 index 000000000..be1fbfc7c --- /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 `open_now: true`: + +1. The server first resolves both identifiers to their respective locations. +2. The server then evaluates the `open_now` hour 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..4f0217810 --- /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 + }, + "amenities": ["curbside_pickup"], + "geo": { + "serves": { + "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 + }, + "amenities": ["curbside_pickup", "in_store_pickup", "parking"], + "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 + }, + "amenities": ["curbside_pickup", "in_store_pickup", "parking"], + "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 + +### Amenity Type + +{{ schema_fields('types/amenity_type', 'location/mcp') }} + +### Location {: #location-entity } + +{{ schema_fields('types/location', 'location/mcp') }} + +### Location Filter {: #location-filter-schema } + +{{ schema_fields('types/location_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..720b99297 --- /dev/null +++ b/docs/specification/location/rest.md @@ -0,0 +1,542 @@ + + +# 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 + }, + "amenities": ["curbside_pickup"], + "geo": { + "serves": { + "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 + }, + "amenities": ["curbside_pickup", "in_store_pickup", "parking"], + "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 + }, + "inventory": [ + { + "id": "item_id_phone_15_pro", + "availability_status": "in_stock" + } + ], + "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 + }, + "amenities": ["curbside_pickup", "in_store_pickup", "parking"], + "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 + }, + "amenities": ["curbside_pickup", "in_store_pickup", "parking"], + "timezone": "America/New_York", + "hours": [ + { + "day": "monday", + "open": "09:00", + "close": "21:00" + }, + { + "day": "tuesday", + "open": "09:00", + "close": "12:00" + }, + { + "day": "tuesday", + "open": "13:00", + "close": "21:00" + }, + { + "day": "wednesday", + "open": "09:00", + "close": "21:00" + }, + { + "day": "thursday", + "open": "09:00", + "close": "21:00" + }, + { + "day": "friday", + "open": "09:00", + "close": "22:00" + }, + { + "day": "saturday", + "open": "10:00", + "close": "20:00" + } + ], + "exception_hours": [ + { + "from": "2026-11-26", + "through": "2026-11-27", + "label": "Thanksgiving", + "open": "00:00", + "close": "00:00" + } + ] + }, + { + "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 + }, + "amenities": ["in_store_pickup"], + "timezone": "America/New_York", + "hours": [ + { + "day": "monday", + "open": "09:00", + "close": "21:00" + }, + { + "day": "tuesday", + "open": "09:00", + "close": "21:00" + }, + { + "day": "wednesday", + "open": "09:00", + "close": "21:00" + }, + { + "day": "thursday", + "open": "09:00", + "close": "21:00" + }, + { + "day": "friday", + "open": "09:00", + "close": "22:00" + } + ], + "exception_hours": [ + { + "from": "2026-11-26", + "through": "2026-11-27", + "label": "Thanksgiving", + "open": "00:00", + "close": "00:00" + } + ] + } + ] + } + ``` + +#### 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 + }, + "amenities": ["curbside_pickup", "in_store_pickup", "parking"], + "timezone": "America/New_York", + "hours": [ + { + "day": "monday", + "open": "09:00", + "close": "21:00" + }, + { + "day": "tuesday", + "open": "09:00", + "close": "21:00" + }, + { + "day": "wednesday", + "open": "09:00", + "close": "21:00" + }, + { + "day": "thursday", + "open": "09:00", + "close": "21:00" + }, + { + "day": "friday", + "open": "09:00", + "close": "22:00" + }, + { + "day": "saturday", + "open": "10:00", + "close": "20:00" + } + ], + "exception_hours": [ + { + "from": "2026-11-26", + "through": "2026-11-27", + "label": "Thanksgiving", + "open": "00:00", + "close": "00:00" + } + ] + } + ], + "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') }} + +### Amenity Type + +{{ schema_fields('types/amenity_type', 'location/rest') }} + +### Location {: #location-entity } + +{{ schema_fields('types/location', 'location/rest') }} + +### Location Filter {: #location-filter-schema } + +{{ schema_fields('types/location_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..3aa4c0e20 --- /dev/null +++ b/docs/specification/location/search.md @@ -0,0 +1,147 @@ + + +# 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, one or more +`filters`, platform-provided user `context` hints, or an extension-defined input. +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. + +> **Implementation guidance:** For processing search requests containing only `context`, +> the following rules **MAY** be followed by businesses: +> +> If the provided `context` is insufficient to determine a location boundary +> (e.g., only country is provided, or context is empty), business **MAY** return a default +> set of locations (e.g., featured locations, or all locations up to a default server-side +> limit) or an empty list. +> +> If the server cannot resolve the location, it **SHOULD** return an error message. + +## 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. All specified amenities **MUST** be supported by the location (AND semantic). + See [Amenity Vocabulary](#amenity-vocabulary) for well-known values. +* **`inventory`** (Array of Objects): Real-time availability of items/goods at + the location. Some industry specific use cases include: + * *Shopping*: Checking stock availability for specific products or variants. + * *Food Ordering*: Checking offering availability of specific dishes or menu items. + Each inventory filter requires an `id` (e.g., product/dish ID) and can optionally specify + a coarse `availability_status` value. + +#### Amenity Vocabulary + +UCP defines an open string vocabulary for amenities via `amenity_type.json` to ensure cross-business +interoperability. Implementations **SHOULD** map their internal features to the well-known types where applicable. + +### 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, business **MUST** use the +> user's address hint provided in the request `context` (which may be coarse/sanitized) +> to derive the center. If `context` is omitted or business is unable to resolve the +> provided address hint, then business **MUST** return an error message with +> `code: "location_geo_filter_resolution_failed"`. + +* **`serves` (Service Area Coverage)**: Filters for locations that can serve + a target destination. The business evaluates coverage using their internal service area rules + (e.g., internal geometry, ZIP code lists) and returns qualifying locations only. + +> **Contextual Fallback**: If the `serves` filter is not explicitly specified in +> the request, the business **MAY** use the user's contextual location hints passed in the +> request `context` object to implicitly apply a `serves` filter, returning only locations +> that can service the user. Similarly to the callout above, if `context` is omitted or +> business is unable to resolve the provided address hint, then business **MUST** return +> an error message with `code: "location_geo_filter_resolution_failed"`. + +## 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 /locations/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 b93477d04..38eddf41b 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 ac5650ee4..cda5cf4fc 100644 --- a/docs/specification/order.md +++ b/docs/specification/order.md @@ -438,7 +438,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 a656fb509..436906e19 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 3c512e000..2bda49466 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -75,6 +75,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 @@ -376,6 +383,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 physical location + discovery models, geographic and geofencing context, operating + hours, and offerings such as amenities and inventory. + - specification/location/search.md: >- + Location discovery via the Location Search capability, specifying + text queries and filters such as operating hours and offerings. + - specification/location/lookup.md: >- + Direct location retrieval via the Location Lookup capability, + specifying identifiers and optional filters. + - 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/scripts/scaffolds/common_location_lookup_request.json b/scripts/scaffolds/common_location_lookup_request.json new file mode 100644 index 000000000..a2eeeab27 --- /dev/null +++ b/scripts/scaffolds/common_location_lookup_request.json @@ -0,0 +1,3 @@ +{ + "ids": ["scaffold_id"] +} diff --git a/scripts/scaffolds/common_location_lookup_response.json b/scripts/scaffolds/common_location_lookup_response.json new file mode 100644 index 000000000..57bc834bd --- /dev/null +++ b/scripts/scaffolds/common_location_lookup_response.json @@ -0,0 +1,6 @@ +{ + "ucp": { + "version": "2026-01-01" + }, + "locations": [] +} diff --git a/scripts/scaffolds/common_location_search_request.json b/scripts/scaffolds/common_location_search_request.json new file mode 100644 index 000000000..fe9fd770a --- /dev/null +++ b/scripts/scaffolds/common_location_search_request.json @@ -0,0 +1,3 @@ +{ + "query": "scaffold" +} diff --git a/scripts/scaffolds/common_location_search_response.json b/scripts/scaffolds/common_location_search_response.json new file mode 100644 index 000000000..57bc834bd --- /dev/null +++ b/scripts/scaffolds/common_location_search_response.json @@ -0,0 +1,6 @@ +{ + "ucp": { + "version": "2026-01-01" + }, + "locations": [] +} 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/common/types/amenity_type.json b/source/schemas/common/types/amenity_type.json new file mode 100644 index 000000000..ca0f3838e --- /dev/null +++ b/source/schemas/common/types/amenity_type.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/amenity_type.json", + "title": "Amenity Type", + "description": "The type of amenity supported by a vertical-agnostic location. Some well-known values are defined below as examples and have standardized semantics; freeform codes are permitted.", + "type": "string", + "examples": [ + "in_store_pickup", + "curbside_pickup", + "drive_through" + ] +} diff --git a/source/schemas/shopping/types/context.json b/source/schemas/common/types/context.json similarity index 93% rename from source/schemas/shopping/types/context.json rename to source/schemas/common/types/context.json index 75659ab79..268369e8a 100644 --- a/source/schemas/shopping/types/context.json +++ b/source/schemas/common/types/context.json @@ -1,13 +1,13 @@ { "$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", "additionalProperties": true, "allOf": [ { - "$ref": "../../common/types/locality.json" + "$ref": "locality.json" }, { "type": "object", @@ -30,7 +30,7 @@ "description": "Buyer claims about eligible benefits such as loyalty membership, payment instrument perks, and similar. Recognized claims MAY inform the Business response (e.g., member-only product availability, adjusted pricing in catalog, provisional discounts at cart or checkout). Businesses MUST ignore unrecognized values without error. Values MUST use reverse-domain naming (e.g., 'com.example.loyalty_gold', 'org.school.student') and MUST be non-identifying.", "uniqueItems": true, "items": { - "$ref": "../../common/types/reverse_domain_name.json" + "$ref": "reverse_domain_name.json" } }, "payment": { @@ -41,7 +41,7 @@ "required": ["handler"], "properties": { "handler": { - "$ref": "../../common/types/reverse_domain_name.json", + "$ref": "reverse_domain_name.json", "description": "Handler registry key advertised in the Business profile's `ucp.payment_handlers`." }, "types": { diff --git a/source/schemas/common/types/daily_hour.json b/source/schemas/common/types/daily_hour.json new file mode 100644 index 000000000..c95428413 --- /dev/null +++ b/source/schemas/common/types/daily_hour.json @@ -0,0 +1,20 @@ +{ + "$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"], + "allOf": [ + { "$ref": "time_interval.json" }, + { + "type": "object", + "properties": { + "day": { + "type": "string", + "enum": ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] + } + } + } + ] +} diff --git a/source/schemas/common/types/exception_hour.json b/source/schemas/common/types/exception_hour.json new file mode 100644 index 000000000..2b8cc6d9e --- /dev/null +++ b/source/schemas/common/types/exception_hour.json @@ -0,0 +1,30 @@ +{ + "$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 range that differs from normal ones (e.g., holiday or temporary change).", + "type": "object", + "required": ["from", "through"], + "allOf": [ + { "$ref": "time_interval.json" }, + { + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "date", + "description": "An ISO 8601 date representing the start of the date range." + }, + "through": { + "type": "string", + "format": "date", + "description": "An ISO 8601 date representing the date after the end of the date range." + }, + "label": { + "type": "string", + "description": "Human readable explanation for the exception (e.g., 'Thanksgiving')." + } + } + } + ] +} diff --git a/source/schemas/common/types/geo.json b/source/schemas/common/types/geo.json new file mode 100644 index 000000000..1810480b3 --- /dev/null +++ b/source/schemas/common/types/geo.json @@ -0,0 +1,22 @@ +{ + "$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." + } + } +} diff --git a/source/schemas/common/types/inventory_filter.json b/source/schemas/common/types/inventory_filter.json new file mode 100644 index 000000000..1166526dd --- /dev/null +++ b/source/schemas/common/types/inventory_filter.json @@ -0,0 +1,19 @@ +{ + "$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 based on its availability status. `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)." + }, + "availability_status": { + "type": "string", + "description": "Availability status of the inventory requested at the location. Well-known values: `in_stock`, `backorder`, `preorder`." + } + }, + "additionalProperties": true +} diff --git a/source/schemas/common/types/location.json b/source/schemas/common/types/location.json new file mode 100644 index 000000000..a8b21ad6a --- /dev/null +++ b/source/schemas/common/types/location.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/location.json", + "title": "Location", + "description": "The full, rich representation of a physical business location. Builds on top of the Base Location schema to include discovery-centric details such as geographic coordinates, operating hours, timezone, and amenities.", + "type": "object", + "additionalProperties": true, + "allOf": [ + { "$ref": "location_base.json" }, + { + "type": "object", + "properties": { + "geo": { + "$ref": "geo.json", + "description": "Geographic coordinates for the location.", + "ucp_request": "omit" + }, + "amenities": { + "type": "array", + "description": "Static features of the location (e.g., 'free_wifi', 'wheelchair_accessible', 'parking', 'curbside_pickup').", + "items": { + "$ref": "amenity_type.json" + }, + "ucp_request": "omit" + }, + "hours": { + "type": "array", + "description": "Regular weekly operating hours. Support split hours through multiple entries on the same day and overnight hours (open > close). Omission of days implies full day closures.", + "items": { + "$ref": "daily_hour.json" + }, + "ucp_request": "omit" + }, + "exception_hours": { + "type": "array", + "description": "Exception hours for specific dates (holidays, closures, etc.). Support split hours through multiple entries on the same day and overnight hours (open > close). For full day closures, MUST explicitly set open and close to 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" + } + }, + "additionalProperties": true + } + ], + "if": { + "anyOf": [ + { "required": ["hours"] }, + { "required": ["exception_hours"] } + ] + }, + "then": { + "required": ["timezone"] + } +} diff --git a/source/schemas/common/types/location_base.json b/source/schemas/common/types/location_base.json new file mode 100644 index 000000000..07975ce24 --- /dev/null +++ b/source/schemas/common/types/location_base.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/location_base.json", + "title": "Location Base", + "description": "A minimalist representation of a physical business location. Contains only the stable identifier, display name, and postal address. Used in contexts (like checkout & catalog lookup) to minimize payload size and avoid exposing unnecessary discovery details.", + "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." + } + } +} diff --git a/source/schemas/common/types/location_filter.json b/source/schemas/common/types/location_filter.json new file mode 100644 index 000000000..bc31f999e --- /dev/null +++ b/source/schemas/common/types/location_filter.json @@ -0,0 +1,83 @@ +{ + "$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 outermost 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 + }, + "serves": { + "type": "object", + "description": "Filter locations that serve the target destination (e.g., delivery area geofence check).", + "properties": { + "point": { + "$ref": "geo.json", + "description": "Coordinates of the target destination." + }, + "address": { + "$ref": "locality.json", + "description": "Coarse postal address reference of the target destination (e.g., for ZIP-code based routing)." + } + }, + "additionalProperties": false, + "oneOf": [ + { "required": ["point"] }, + { "required": ["address"] } + ] + } + }, + "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 + }, + "amenities": { + "type": "array", + "items": { + "$ref": "amenity_type.json" + }, + "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 dfd58d0de..85421d863 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..c9d2d568b --- /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.", + "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]$", + "description": "End time (e.g., '18:00')." + } + } +} diff --git a/source/schemas/shopping/cart.json b/source/schemas/shopping/cart.json index 1dc76fa11..9b0fa4436 100644 --- a/source/schemas/shopping/cart.json +++ b/source/schemas/shopping/cart.json @@ -68,7 +68,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", @@ -76,7 +76,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 d5298cd12..64b514a0a 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" @@ -124,10 +124,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 5badd714e..377cf98f1 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 e9093b0c9..1c2ab9197 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.json b/source/schemas/shopping/types/fulfillment.json index 6aa8c0589..1150baf52 100644 --- a/source/schemas/shopping/types/fulfillment.json +++ b/source/schemas/shopping/types/fulfillment.json @@ -4,7 +4,6 @@ "title": "Fulfillment", "description": "Container for fulfillment methods and availability.", "type": "object", - "ucp_shared_request": true, "properties": { "methods": { "type": "array", diff --git a/source/schemas/shopping/types/fulfillment_available_method.json b/source/schemas/shopping/types/fulfillment_available_method.json index 596293d8e..7d1a7a4db 100644 --- a/source/schemas/shopping/types/fulfillment_available_method.json +++ b/source/schemas/shopping/types/fulfillment_available_method.json @@ -4,7 +4,6 @@ "title": "Fulfillment Available Method", "description": "Inventory availability hint for a fulfillment method type.", "type": "object", - "ucp_shared_request": true, "required": ["type", "line_item_ids"], "additionalProperties": true, "properties": { diff --git a/source/schemas/shopping/types/fulfillment_destination.json b/source/schemas/shopping/types/fulfillment_destination.json index cc6708dc9..d9af73dd0 100644 --- a/source/schemas/shopping/types/fulfillment_destination.json +++ b/source/schemas/shopping/types/fulfillment_destination.json @@ -4,13 +4,13 @@ "title": "Fulfillment Destination", "description": "A destination for fulfillment.", "type": "object", - "ucp_shared_request": true, "oneOf": [ { "$ref": "shipping_destination.json" }, { - "$ref": "retail_location.json" + "$ref": "../../common/types/location_base.json", + "description": "A pickup retail location (e.g. store, locker, etc.)." } ] } diff --git a/source/schemas/shopping/types/fulfillment_option.json b/source/schemas/shopping/types/fulfillment_option.json index 05503be54..788cb2757 100644 --- a/source/schemas/shopping/types/fulfillment_option.json +++ b/source/schemas/shopping/types/fulfillment_option.json @@ -4,7 +4,6 @@ "title": "Fulfillment Option", "description": "A fulfillment option within a group (e.g., Standard Shipping $5, Express $15). Extends the fulfillment option base with cost and timing.", "type": "object", - "ucp_shared_request": true, "allOf": [ { "$ref": "fulfillment_option_base.json" diff --git a/source/schemas/shopping/types/fulfillment_option_base.json b/source/schemas/shopping/types/fulfillment_option_base.json index 1f7367d32..0eeebee77 100644 --- a/source/schemas/shopping/types/fulfillment_option_base.json +++ b/source/schemas/shopping/types/fulfillment_option_base.json @@ -4,7 +4,6 @@ "title": "Fulfillment Option Base", "description": "Common base for a fulfillment option: an addressable, renderable choice (e.g. Standard, Express). Catalog uses this base directly; checkout composes it with cost and timing.", "type": "object", - "ucp_shared_request": true, "required": ["id", "title"], "additionalProperties": true, "properties": { 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/schemas/shopping/types/shipping_destination.json b/source/schemas/shopping/types/shipping_destination.json index ec77b9789..6acdf1356 100644 --- a/source/schemas/shopping/types/shipping_destination.json +++ b/source/schemas/shopping/types/shipping_destination.json @@ -4,7 +4,6 @@ "title": "Shipping Destination", "description": "Shipping destination.", "type": "object", - "ucp_shared_request": true, "allOf": [ { "$ref": "../../common/types/postal_address.json" diff --git a/source/services/common/mcp.openrpc.json b/source/services/common/mcp.openrpc.json new file mode 100644 index 000000000..5c7518b1d --- /dev/null +++ b/source/services/common/mcp.openrpc.json @@ -0,0 +1,105 @@ +{ + "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": "RFC 9421 HTTP message signature." + }, + "signature_input": { + "type": "string", + "description": "RFC 9421 Signature-Input header. Format: `sig1=(\"@method\" \"@path\" ...);created=;keyid=\"\"`." + }, + "content-digest": { + "type": "string", + "description": "Body digest per RFC 9530. Format: `sha-256=::`." + } + } + } + } + }, + "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" + } + } + } +}