diff --git a/.cspell.json b/.cspell.json index 27f65385..5ad53a27 100644 --- a/.cspell.json +++ b/.cspell.json @@ -18,7 +18,8 @@ "ignoreRegExpList": [ "src\\s*=\\s*(\"[^\"]*\"|'[^']*')", "(\"token\"|token)\\s*:\\s*(\"[^\"]*\"|'[^']*')", - "(\"auth_jwt\"|auth_jwt)\\s*:\\s*(\"[^\"]*\"|'[^']*')" + "(\"auth_jwt\"|auth_jwt)\\s*:\\s*(\"[^\"]*\"|'[^']*')", + "(\"cursor\"|cursor)\\s*:\\s*(\"[^\"]*\"|'[^']*')" ], "dictionaryDefinitions": [ { diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index bfae6fd2..05b7b72c 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -35,15 +35,19 @@ Shopee Splitit Streamable Stripe +Talla Target UCP Ulta Visa +Variante Wayfair Worldpay +Zapatillas Zalando adyen agentic +amortiguación atok backorder checkout @@ -59,10 +63,12 @@ gpay ingestions inlinehilite lifecycles +ligeras linenums llms llmstxt mastercard +midsole mkdocs mtok openapi @@ -74,14 +80,19 @@ preorders proto protobuf pymdownx +reactiva renderable repudiable schemas sdjwt shopify superfences +talla +tracción upsell upsells +variante vulnz yaml +zapatillas yml diff --git a/docs/specification/catalog/index.md b/docs/specification/catalog/index.md new file mode 100644 index 00000000..6ebf6320 --- /dev/null +++ b/docs/specification/catalog/index.md @@ -0,0 +1,232 @@ + + +# Catalog Capability + +## Overview + +The Catalog capability allows platforms to search and browse business product catalogs. +This enables product discovery before checkout, supporting use cases like: + +* Free-text product search +* Category and filter-based browsing +* Batch product/variant retrieval by identifier +* Price comparison across variants + +## Capabilities + +| Capability | Description | +| :--- | :--- | +| [`dev.ucp.shopping.catalog.search`](search.md) | Search for products using query text and filters. | +| [`dev.ucp.shopping.catalog.lookup`](lookup.md) | Retrieve products or variants by identifier. | + +## Key Concepts + +* **Product**: A catalog item with title, description, media, and one or more + variants. +* **Variant**: A purchasable item with specific option selections (e.g., "Blue / + Large"), price, and availability. +* **Price**: Price values include both amount (in minor currency units) and + currency code, enabling multi-currency catalogs. + +### Relationship to Checkout + +Catalog operations return product and variant IDs that can be used directly in +checkout `line_items[].item.id`. The variant ID from catalog retrieval should match +the item ID expected by checkout. + +## Shared Entities + +### Context + +Location and market context for catalog operations. All fields are optional +hints for relevance and localization. Platforms MAY geo-detect context from +request headers. + +Context signals are provisional—not authorization. Businesses SHOULD use these +values when authoritative data (e.g., shipping address) is absent, and MAY +ignore or down-rank them if inconsistent with stronger signals (authenticated +account, fraud rules, export controls). Eligibility and policy enforcement +MUST occur at checkout/order time with authoritative data. + +Businesses determine market assignment—including currency—based on context +signals. Price filter values are interpreted in the business's assigned +currency; response prices include explicit currency codes. + +{{ schema_fields('types/context', 'catalog') }} + +### Product + +A catalog item representing a sellable item with one or more purchasable variants. + +`media` and `variants` are ordered arrays. Businesses SHOULD return the most +relevant variant and image first—default for lookups, best match based on query +and context for search. Platforms SHOULD treat the first element as featured. + +{{ schema_fields('types/product', 'catalog') }} + +### Variant + +A purchasable item with specific option selections, price, and availability. + +`media` is an ordered array. Businesses SHOULD return the featured variant image +as the first element. Platforms SHOULD treat the first element as featured. + +{{ schema_fields('types/variant', 'catalog') }} + +### Price + +{{ schema_fields('types/price', 'catalog') }} + +### Price Range + +{{ schema_fields('types/price_range', 'catalog') }} + +### Media + +{{ schema_fields('types/media', 'catalog') }} + +### Product Option + +{{ schema_fields('types/product_option', 'catalog') }} + +### Option Value + +{{ schema_fields('types/option_value', 'catalog') }} + +### Selected Option + +{{ schema_fields('types/selected_option', 'catalog') }} + +### Rating + +{{ schema_fields('types/rating', 'catalog') }} + +## Messages and Error Handling + +All catalog 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 | `NOT_FOUND`, `OUT_OF_STOCK`, `REGION_RESTRICTED` | +| `warning` | Important conditions affecting purchase | `DELAYED_FULFILLMENT`, `FINAL_SALE`, `AGE_RESTRICTED` | +| `info` | Additional context without issues | `PROMOTIONAL_PRICING`, `LIMITED_AVAILABILITY` | + +**Note**: All 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": {...}, + "products": [] +} +``` + +This is not an error - the query was valid but returned no results. + +#### Backorder Warning + +When a product is available but has delayed fulfillment, return the product with +a warning message. Use the `path` field to target specific variants. + +```json +{ + "ucp": {...}, + "products": [ + { + "id": "prod_xyz789", + "title": "Professional Chef Knife Set", + "description": { "plain": "Complete professional knife collection." }, + "price_range": { + "min": { "amount": 29900, "currency": "USD" }, + "max": { "amount": 29900, "currency": "USD" } + }, + "variants": [ + { + "id": "var_abc", + "title": "12-piece Set", + "description": { "plain": "Complete professional knife collection." }, + "price": { "amount": 29900, "currency": "USD" }, + "availability": { "available": true } + } + ] + } + ], + "messages": [ + { + "type": "warning", + "code": "delayed_fulfillment", + "path": "$.products[0].variants[0]", + "content": "12-piece set on backorder, ships in 2-3 weeks" + } + ] +} +``` + +Agents can present the option and inform the user about the delay. The `path` +field uses RFC 9535 JSONPath to target specific components. + +#### Identifiers Not Found + +When requested identifiers don't exist, return success with the found products +(if any). The response MAY include informational messages indicating which +identifiers were not found. + +```json +{ + "ucp": {...}, + "products": [], + "messages": [ + { + "type": "info", + "code": "not_found", + "content": "prod_invalid" + } + ] +} +``` + +Agents correlate results by matching fields in returned products against +requested identifiers. + +## 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/catalog/lookup.md b/docs/specification/catalog/lookup.md new file mode 100644 index 00000000..fc7a3300 --- /dev/null +++ b/docs/specification/catalog/lookup.md @@ -0,0 +1,79 @@ + + +# Catalog Lookup Capability + +* **Capability Name:** `dev.ucp.shopping.catalog.lookup` + +Retrieves products or variants by identifier. Use this when you already have +identifiers (e.g., from a saved list, deep links, or cart validation). + +## Operation + +| Operation | Description | +| :--- | :--- | +| **Lookup Catalog** | Retrieve products or variants by identifier. | + +### Supported Identifiers + +The `ids` parameter accepts an array of identifiers. Implementations MUST support +lookup by product ID and variant ID. Implementations MAY additionally support +secondary identifiers such as SKU or handle, provided these are also fields on +the returned product object. + +Duplicate identifiers in the request MUST be deduplicated. When an identifier +matches multiple products (e.g., a SKU shared across variants), all matching +products MUST be returned. When multiple identifiers resolve to the same product, +it MUST be returned once. The `products` array may contain fewer or more items +than requested identifiers. + +### Client Correlation + +The response does not guarantee order or provide explicit identifier-to-product +mapping. Clients correlate results by matching fields in the returned products +(e.g., `id`, `sku`, `handle`) against the requested identifiers. + +### 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). + +### Resolution Behavior + +For each identifier, the response returns the parent product with full context +(title, description, media, options): + +* **Product ID lookup**: `variants` MAY contain a representative set. +* **Variant ID lookup**: `variants` MUST contain only the requested variant. + +When the variant set is large, a representative set MAY be returned based on +buyer context or other criteria. This ensures agents always have product context +for display while getting exactly what they requested. + +### Request + +{{ extension_schema_fields('catalog_lookup.json#/$defs/lookup_request', 'catalog') }} + +### Response + +{{ extension_schema_fields('catalog_lookup.json#/$defs/lookup_response', 'catalog') }} + +## Transport Bindings + +* [REST Binding](rest.md#post-cataloglookup): `POST /catalog/lookup` +* [MCP Binding](mcp.md#lookup_catalog): `lookup_catalog` tool diff --git a/docs/specification/catalog/mcp.md b/docs/specification/catalog/mcp.md new file mode 100644 index 00000000..5dd809b3 --- /dev/null +++ b/docs/specification/catalog/mcp.md @@ -0,0 +1,475 @@ + + +# Catalog - MCP Binding + +This document specifies the Model Context Protocol (MCP) binding for the +[Catalog Capability](index.md). + +## Protocol Fundamentals + +### Discovery + +Businesses advertise MCP transport availability through their UCP profile at +`/.well-known/ucp`. + +```json +{ + "ucp": { + "version": "2026-01-11", + "services": { + "dev.ucp.shopping": { + "version": "2026-01-11", + "spec": "https://ucp.dev/specification/overview", + "mcp": { + "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json", + "endpoint": "https://business.example.com/ucp/mcp" + } + } + }, + "capabilities": [ + { + "name": "dev.ucp.shopping.catalog.search", + "version": "2026-01-11", + "spec": "https://ucp.dev/specification/catalog/search", + "schema": "https://ucp.dev/schemas/shopping/catalog_search.json" + }, + { + "name": "dev.ucp.shopping.catalog.lookup", + "version": "2026-01-11", + "spec": "https://ucp.dev/specification/catalog/lookup", + "schema": "https://ucp.dev/schemas/shopping/catalog_lookup.json" + } + ] + } +} +``` + +### 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_catalog", + "arguments": { + "meta": { + "ucp-agent": { + "profile": "https://platform.example/profiles/v2026-01/shopping-agent.json" + } + }, + "catalog": { + "query": "blue running shoes", + "context": { + "country": "US", + "intent": "looking for comfortable everyday shoes" + } + } + } + } +} +``` + +The `meta["ucp-agent"]` field is **required** on all requests to enable +version compatibility checking and capability negotiation. + +## Tools + +| Tool | Capability | Description | +| :--- | :--- | :--- | +| `search_catalog` | [Search](search.md) | Search for products. | +| `lookup_catalog` | [Lookup](lookup.md) | Lookup one or more products or variants by identifier. | + +### `search_catalog` + +Maps to the [Catalog Search](search.md) capability. + +{{ method_fields('search_catalog', 'mcp.openrpc.json', 'catalog-mcp') }} + +#### Example + +=== "Request" + + ```json + { + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "search_catalog", + "arguments": { + "meta": { + "ucp-agent": { + "profile": "https://platform.example/profiles/v2026-01/shopping-agent.json" + } + }, + "catalog": { + "query": "blue running shoes", + "context": { + "country": "US", + "region": "CA", + "intent": "looking for comfortable everyday shoes" + }, + "filters": { + "category": ["Footwear"], + "price": { + "max": 15000 + } + }, + "pagination": { + "limit": 20 + } + } + } + } + } + ``` + +=== "Response" + + ```json + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "structuredContent": { + "ucp": { + "version": "2026-01-11", + "capabilities": { + "dev.ucp.shopping.catalog.search": [ + {"version": "2026-01-11"} + ] + } + }, + "products": [ + { + "id": "prod_abc123", + "handle": "blue-runner-pro", + "title": "Blue Runner Pro", + "description": { + "plain": "Lightweight running shoes with responsive cushioning." + }, + "url": "https://business.example.com/products/blue-runner-pro", + "category": [ + { "value": "187", "taxonomy": "google_product_category" }, + { "value": "aa-8-1", "taxonomy": "shopify" }, + { "value": "Footwear > Running", "taxonomy": "merchant" } + ], + "price_range": { + "min": { "amount": 12000, "currency": "USD" }, + "max": { "amount": 12000, "currency": "USD" } + }, + "media": [ + { + "type": "image", + "url": "https://cdn.example.com/products/blue-runner-pro.jpg", + "alt_text": "Blue Runner Pro running shoes" + } + ], + "options": [ + { + "name": "Size", + "values": [{"label": "8"}, {"label": "9"}, {"label": "10"}, {"label": "11"}, {"label": "12"}] + } + ], + "variants": [ + { + "id": "prod_abc123_size10", + "sku": "BRP-BLU-10", + "title": "Size 10", + "description": { "plain": "Size 10 variant" }, + "price": { "amount": 12000, "currency": "USD" }, + "availability": { "available": true }, + "selected_options": [ + { "name": "Size", "label": "10" } + ], + "tags": ["running", "road", "neutral"], + "seller": { + "name": "Example Store", + "links": [ + { "type": "refund_policy", "url": "https://business.example.com/policies/refunds" } + ] + } + } + ], + "rating": { + "value": 4.5, + "scale_max": 5, + "count": 128 + }, + "metadata": { + "collection": "Winter 2026", + "technology": { + "midsole": "React foam", + "outsole": "Continental rubber" + } + } + } + ], + "pagination": { + "cursor": "eyJwYWdlIjoxfQ==", + "has_next_page": true, + "total_count": 47 + } + } + } + } + ``` + +### `lookup_catalog` + +Maps to the [Catalog Lookup](lookup.md) capability. See capability documentation +for supported identifiers, resolution behavior, and client correlation requirements. + +The `catalog.ids` parameter accepts an array of identifiers and optional context. + +{{ method_fields('lookup_catalog', 'mcp.openrpc.json', 'catalog-mcp') }} + +#### Example + +=== "Request" + + ```json + { + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": { + "name": "lookup_catalog", + "arguments": { + "meta": { + "ucp-agent": { + "profile": "https://platform.example/profiles/v2026-01/shopping-agent.json" + } + }, + "catalog": { + "ids": ["prod_abc123", "var_xyz789"], + "context": { + "country": "US" + } + } + } + } + } + ``` + +=== "Response" + + ```json + { + "jsonrpc": "2.0", + "id": 2, + "result": { + "structuredContent": { + "ucp": { + "version": "2026-01-11", + "capabilities": { + "dev.ucp.shopping.catalog.lookup": [ + {"version": "2026-01-11"} + ] + } + }, + "products": [ + { + "id": "prod_abc123", + "title": "Blue Runner Pro", + "description": { + "plain": "Lightweight running shoes with responsive cushioning." + }, + "price_range": { + "min": { "amount": 12000, "currency": "USD" }, + "max": { "amount": 12000, "currency": "USD" } + }, + "variants": [ + { + "id": "prod_abc123_size10", + "sku": "BRP-BLU-10", + "title": "Size 10", + "description": { "plain": "Size 10 variant" }, + "price": { "amount": 12000, "currency": "USD" }, + "availability": { "available": true }, + "tags": ["running", "road", "neutral"], + "seller": { + "name": "Example Store", + "links": [ + { "type": "refund_policy", "url": "https://business.example.com/policies/refunds" } + ] + } + } + ], + "metadata": { + "collection": "Winter 2026", + "technology": { + "midsole": "React foam", + "outsole": "Continental rubber" + } + } + }, + { + "id": "prod_def456", + "title": "Trail Master X", + "description": { + "plain": "Rugged trail running shoes with aggressive tread." + }, + "price_range": { + "min": { "amount": 15000, "currency": "USD" }, + "max": { "amount": 15000, "currency": "USD" } + }, + "variants": [ + { + "id": "var_xyz789", + "sku": "TMX-GRN-11", + "title": "Size 11 - Green", + "description": { "plain": "Size 11 Green variant" }, + "price": { "amount": 15000, "currency": "USD" }, + "availability": { "available": true }, + "tags": ["trail", "waterproof"], + "seller": { + "name": "Example Store" + } + } + ] + } + ] + } + } + } + ``` + +#### Partial Success + +When some identifiers are not found, the response includes the found products. The +response MAY include informational messages indicating which identifiers were not found. + +```json +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "structuredContent": { + "ucp": { + "version": "2026-01-11", + "capabilities": { + "dev.ucp.shopping.catalog.lookup": [ + {"version": "2026-01-11"} + ] + } + }, + "products": [ + { + "id": "prod_abc123", + "title": "Blue Runner Pro", + "price_range": { + "min": { "amount": 12000, "currency": "USD" }, + "max": { "amount": 12000, "currency": "USD" } + }, + "variants": [] + } + ], + "messages": [ + { + "type": "info", + "code": "not_found", + "content": "prod_notfound1" + }, + { + "type": "info", + "code": "not_found", + "content": "prod_notfound2" + } + ] + } + } +} +``` + +## Error Handling + +UCP uses a two-layer error model separating transport errors from business outcomes. + +### Transport Errors + +Use JSON-RPC 2.0 error codes for protocol-level issues that prevent request processing: + +| Code | Meaning | +| :--- | :--- | +| -32600 | Invalid Request - Malformed JSON-RPC | +| -32601 | Method not found | +| -32602 | Invalid params - Missing required parameter | +| -32603 | Internal error | + +### Business Outcomes + +All application-level outcomes return a successful JSON-RPC result with the UCP +envelope and optional `messages` array. See [Catalog Overview](index.md#messages-and-error-handling) +for message semantics and common scenarios. + +#### Example: All Products Not Found + +When all requested identifiers fail to resolve, the response contains an empty `products` +array. The response MAY include informational messages indicating which identifiers were +not found. + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "structuredContent": { + "ucp": { + "version": "2026-01-11", + "capabilities": { + "dev.ucp.shopping.catalog.lookup": [ + {"version": "2026-01-11"} + ] + } + }, + "products": [], + "messages": [ + { + "type": "info", + "code": "not_found", + "content": "prod_invalid" + } + ] + } + } +} +``` + +Business outcomes use the JSON-RPC `result` field with messages in the response +payload. See the [Partial Success](#partial-success) section for handling mixed +results. + +## Conformance + +A conforming MCP transport implementation **MUST**: + +1. Implement JSON-RPC 2.0 protocol correctly. +2. Provide both `search_catalog` and `lookup_catalog` tools. +3. Require `catalog.query` parameter for `search_catalog`. +4. Implement `lookup_catalog` per [Catalog Lookup](lookup.md) capability requirements. +5. Use JSON-RPC errors for transport issues; use `messages` array for business outcomes. +6. Return successful result for lookup requests; unknown identifiers result in fewer products returned (MAY include informational `not_found` messages). +7. Validate tool inputs against UCP schemas. +8. Return products with valid `Price` objects (amount + currency). +9. Support cursor-based pagination with default limit of 10. +10. Return `-32602` (Invalid params) for requests exceeding batch size limits. diff --git a/docs/specification/catalog/rest.md b/docs/specification/catalog/rest.md new file mode 100644 index 00000000..6245a330 --- /dev/null +++ b/docs/specification/catalog/rest.md @@ -0,0 +1,390 @@ + + +# Catalog - REST Binding + +This document specifies the HTTP/REST binding for the +[Catalog Capability](index.md). + +## Protocol Fundamentals + +### Discovery + +Businesses advertise REST transport availability through their UCP profile at +`/.well-known/ucp`. + +```json +{ + "ucp": { + "version": "2026-01-11", + "services": { + "dev.ucp.shopping": { + "version": "2026-01-11", + "spec": "https://ucp.dev/specification/overview", + "rest": { + "schema": "https://ucp.dev/services/shopping/rest.openapi.json", + "endpoint": "https://business.example.com/ucp" + } + } + }, + "capabilities": [ + { + "name": "dev.ucp.shopping.catalog.search", + "version": "2026-01-11", + "spec": "https://ucp.dev/specification/catalog/search", + "schema": "https://ucp.dev/schemas/shopping/catalog_search.json" + }, + { + "name": "dev.ucp.shopping.catalog.lookup", + "version": "2026-01-11", + "spec": "https://ucp.dev/specification/catalog/lookup", + "schema": "https://ucp.dev/schemas/shopping/catalog_lookup.json" + } + ] + } +} +``` + +## Endpoints + +| Endpoint | Method | Capability | Description | +| :--- | :--- | :--- | :--- | +| `/catalog/search` | POST | [Search](search.md) | Search for products. | +| `/catalog/lookup` | POST | [Lookup](lookup.md) | Lookup one or more products by ID. | + +### `POST /catalog/search` + +Maps to the [Catalog Search](search.md) capability. + +{{ method_fields('search_catalog', 'rest.openapi.json', 'catalog-rest') }} + +#### Example + +=== "Request" + + ```json + { + "query": "blue running shoes", + "context": { + "country": "US", + "region": "CA", + "intent": "looking for comfortable everyday shoes" + }, + "filters": { + "category": ["Footwear"], + "price": { + "max": 15000 + } + }, + "pagination": { + "limit": 20 + } + } + ``` + +=== "Response" + + ```json + { + "ucp": { + "version": "2026-01-11", + "capabilities": [ + { + "name": "dev.ucp.shopping.catalog.search", + "version": "2026-01-11" + } + ] + }, + "products": [ + { + "id": "prod_abc123", + "handle": "blue-runner-pro", + "title": "Blue Runner Pro", + "description": { + "plain": "Lightweight running shoes with responsive cushioning." + }, + "url": "https://business.example.com/products/blue-runner-pro", + "category": [ + { "value": "187", "taxonomy": "google_product_category" }, + { "value": "aa-8-1", "taxonomy": "shopify" }, + { "value": "Footwear > Running", "taxonomy": "merchant" } + ], + "price_range": { + "min": { "amount": 12000, "currency": "USD" }, + "max": { "amount": 12000, "currency": "USD" } + }, + "media": [ + { + "type": "image", + "url": "https://cdn.example.com/products/blue-runner-pro.jpg", + "alt_text": "Blue Runner Pro running shoes" + } + ], + "options": [ + { + "name": "Size", + "values": [{"label": "8"}, {"label": "9"}, {"label": "10"}, {"label": "11"}, {"label": "12"}] + } + ], + "variants": [ + { + "id": "prod_abc123_size10", + "sku": "BRP-BLU-10", + "title": "Size 10", + "description": { "plain": "Size 10 variant" }, + "price": { "amount": 12000, "currency": "USD" }, + "availability": { "available": true }, + "selected_options": [ + { "name": "Size", "label": "10" } + ], + "tags": ["running", "road", "neutral"], + "seller": { + "name": "Example Store", + "links": [ + { "type": "refund_policy", "url": "https://business.example.com/policies/refunds" } + ] + } + } + ], + "rating": { + "value": 4.5, + "scale_max": 5, + "count": 128 + }, + "metadata": { + "collection": "Winter 2026", + "technology": { + "midsole": "React foam", + "outsole": "Continental rubber" + } + } + } + ], + "pagination": { + "cursor": "eyJwYWdlIjoxfQ==", + "has_next_page": true, + "total_count": 47 + } + } + ``` + +### `POST /catalog/lookup` + +Maps to the [Catalog Lookup](lookup.md) capability. See capability documentation +for supported identifiers, resolution behavior, and client correlation requirements. + +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') }} + +#### Example: Batch Lookup with Context + +=== "Request" + + ```json + POST /catalog/lookup HTTP/1.1 + Host: business.example.com + Content-Type: application/json + + { + "ids": ["prod_abc123", "prod_def456"], + "context": { + "country": "US", + "language": "es" + } + } + ``` + +=== "Response" + + ```json + { + "ucp": { + "version": "2026-01-11", + "capabilities": [ + { + "name": "dev.ucp.shopping.catalog.lookup", + "version": "2026-01-11" + } + ] + }, + "products": [ + { + "id": "prod_abc123", + "title": "Blue Runner Pro", + "description": { + "plain": "Zapatillas ligeras con amortiguación reactiva." + }, + "price_range": { + "min": { "amount": 12000, "currency": "USD" }, + "max": { "amount": 12000, "currency": "USD" } + }, + "variants": [ + { + "id": "prod_abc123_size10", + "sku": "BRP-BLU-10", + "title": "Talla 10", + "description": { "plain": "Variante talla 10" }, + "price": { "amount": 12000, "currency": "USD" }, + "availability": { "available": true } + } + ] + }, + { + "id": "prod_def456", + "title": "Trail Blazer X", + "description": { + "plain": "Zapatillas de trail con tracción superior." + }, + "price_range": { + "min": { "amount": 15000, "currency": "USD" }, + "max": { "amount": 15000, "currency": "USD" } + }, + "variants": [ + { + "id": "prod_def456_size10", + "sku": "TBX-GRN-10", + "title": "Talla 10", + "price": { "amount": 15000, "currency": "USD" }, + "availability": { "available": true } + } + ] + } + ] + } + ``` + +#### Example: Partial Success (Some Identifiers Not Found) + +When some identifiers in the batch are not found, the response includes the +found products in the `products` array. The response MAY include informational +messages indicating which identifiers were not found. + +=== "Request" + + ```json + { + "ids": ["prod_abc123", "prod_invalid", "prod_def456"] + } + ``` + +=== "Response" + + ```json + { + "ucp": { + "version": "2026-01-11", + "capabilities": [ + { + "name": "dev.ucp.shopping.catalog.lookup", + "version": "2026-01-11" + } + ] + }, + "products": [ + { + "id": "prod_abc123", + "title": "Blue Runner Pro", + "price_range": { + "min": { "amount": 12000, "currency": "USD" }, + "max": { "amount": 12000, "currency": "USD" } + } + }, + { + "id": "prod_def456", + "title": "Trail Blazer X", + "price_range": { + "min": { "amount": 15000, "currency": "USD" }, + "max": { "amount": 15000, "currency": "USD" } + } + } + ], + "messages": [ + { + "type": "info", + "code": "not_found", + "content": "prod_invalid" + } + ] + } + ``` + +## Error Handling + +UCP uses a two-layer error model separating transport 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 [Catalog Overview](index.md#messages-and-error-handling) +for message semantics and common scenarios. + +#### Example: All Products Not Found + +When all requested identifiers fail lookup, the `products` array is empty. The response +MAY include informational messages indicating which identifiers were not found. + +```json +{ + "ucp": { + "version": "2026-01-11", + "capabilities": [ + { + "name": "dev.ucp.shopping.catalog.lookup", + "version": "2026-01-11" + } + ] + }, + "products": [], + "messages": [ + { + "type": "info", + "code": "not_found", + "content": "prod_invalid1" + }, + { + "type": "info", + "code": "not_found", + "content": "prod_invalid2" + } + ] +} +``` + +Business outcomes use the standard HTTP 200 status with messages in the response body. + +## Conformance + +A conforming REST transport implementation **MUST**: + +1. Implement the `POST /catalog/search` endpoint with required `query` parameter. +2. Implement the `POST /catalog/lookup` endpoint per [Catalog Lookup](lookup.md) capability requirements. +3. Return products with valid `Price` objects (amount + currency). +4. Support cursor-based pagination with default limit of 10. +5. Return HTTP 200 for lookup requests; unknown identifiers result in fewer products returned (MAY include informational `not_found` messages). +6. Return HTTP 400 with `request_too_large` error for requests exceeding batch size limits. diff --git a/docs/specification/catalog/search.md b/docs/specification/catalog/search.md new file mode 100644 index 00000000..933b99c3 --- /dev/null +++ b/docs/specification/catalog/search.md @@ -0,0 +1,64 @@ + + +# Catalog Search Capability + +* **Capability Name:** `dev.ucp.shopping.catalog.search` + +Performs a search against the business's product catalog. Supports free-text +queries, filtering by category and price, and pagination. + +## Operation + +| Operation | Description | +| :--- | :--- | +| **Search Catalog** | Search for products using query text and filters. | + +### Request + +{{ extension_schema_fields('catalog_search.json#/$defs/search_request', 'catalog') }} + +### Response + +{{ extension_schema_fields('catalog_search.json#/$defs/search_response', 'catalog') }} + +## Search Filters + +Filter criteria for narrowing search results. Standard filters are defined below; +merchants MAY support additional custom filters via `additionalProperties`. + +{{ schema_fields('types/search_filters', 'catalog') }} + +### Price Filter + +{{ schema_fields('types/price_filter', 'catalog') }} + +## Pagination + +Cursor-based pagination for list operations. + +### Pagination Request + +{{ extension_schema_fields('types/pagination.json#/$defs/request', 'catalog') }} + +### Pagination Response + +{{ extension_schema_fields('types/pagination.json#/$defs/response', 'catalog') }} + +## Transport Bindings + +* [REST Binding](rest.md#post-catalogsearch): `POST /catalog/search` +* [MCP Binding](mcp.md#search_catalog): `search_catalog` tool diff --git a/docs/specification/checkout.md b/docs/specification/checkout.md index 06a396bb..3e551ea7 100644 --- a/docs/specification/checkout.md +++ b/docs/specification/checkout.md @@ -365,10 +365,11 @@ defined below: ### Context -Context signals are provisional hints. Businesses SHOULD use these values when -authoritative data (e.g. address) is absent, and MAY ignore unsupported values -without returning errors. This differs from authoritative selections which -require explicit validation and error feedback. +Context signals are provisional—not authorization. Businesses SHOULD use these +values when authoritative data (e.g., shipping address) is absent, and MAY +ignore or down-rank them if inconsistent with stronger signals (authenticated +account, fraud rules, export controls). Eligibility and policy enforcement +MUST occur at checkout/order time with authoritative data. {{ schema_fields('context', 'checkout') }} diff --git a/main.py b/main.py index a6c74f60..f19d3c4d 100644 --- a/main.py +++ b/main.py @@ -277,7 +277,8 @@ def create_link(ref_string, spec_file_name, context=None): Args: ---- - ref_string: e.g., "types/line_item.create_req.json" + ref_string: e.g., "types/line_item.create_req.json" or + "types/pagination.json#/$defs/response" spec_file_name: e.g., "checkout" context: Optional dict with 'io_type' (request/response) for polymorphic type handling. @@ -296,7 +297,14 @@ def create_link(ref_string, spec_file_name, context=None): ): spec_file_name = "checkout" - filename = Path(ref_string).name + # Extract fragment identifier if present (e.g., #/$defs/response) + # This handles cases like "types/pagination.json#/$defs/response" + fragment = None + ref_path = ref_string + if "#/$defs/" in ref_string: + ref_path, fragment = ref_string.split("#/$defs/", 1) + + filename = Path(ref_path).name # Check if this reference comes from the core UCP schema is_ucp = "ucp.json" in ref_string @@ -308,9 +316,20 @@ def create_link(ref_string, spec_file_name, context=None): # 2. Generate Link Text (Visual) # e.g. "checkout_response" -> "Checkout Response" - link_text = ( - raw_name.replace("_", " ").replace(".", " ").replace("-", " ").title() - ) + # e.g. "pagination" + fragment "response" -> "Pagination Response" + if fragment: + base_text = ( + raw_name.replace("_", " ").replace(".", " ").replace("-", " ").title() + ) + fragment_text = ( + fragment.replace("_", " ").replace(".", " ").replace("-", " ").title() + ) + link_text = f"{base_text} {fragment_text}" + else: + link_text = ( + raw_name.replace("_", " ").replace(".", " ").replace("-", " ").title() + ) + if link_text.endswith("Resp"): link_text = link_text.replace("Resp", "Response") elif link_text.endswith("Req"): @@ -330,7 +349,15 @@ def create_link(ref_string, spec_file_name, context=None): anchor_name = base_entity.replace("_", "-") - if len(parts) > 1: + # Handle fragment in anchor + # e.g., pagination#/$defs/response -> pagination-response + if fragment: + fragment_anchor = fragment.replace("_", "-") + if anchor_name: # External ref: base-fragment + anchor_name = f"{anchor_name}-{fragment_anchor}" + else: # Internal ref like #/$defs/context: just use fragment + anchor_name = fragment_anchor + elif len(parts) > 1: variant = parts[1] variant_expanded = ( variant.replace("create_req", "create-request") diff --git a/mkdocs.yml b/mkdocs.yml index 6686c99c..72cc66a6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -44,6 +44,12 @@ nav: - Overview: specification/cart.md - HTTP/REST Binding: specification/cart-rest.md - MCP Binding: specification/cart-mcp.md + - Catalog Capability: + - Overview: specification/catalog/index.md + - Search: specification/catalog/search.md + - Lookup: specification/catalog/lookup.md + - HTTP/REST Binding: specification/catalog/rest.md + - MCP Binding: specification/catalog/mcp.md - Order Capability: specification/order.md - Identity Linking Capability: specification/identity-linking.md - Payment Handlers: @@ -212,6 +218,12 @@ plugins: - specification/cart.md - specification/cart-rest.md - specification/cart-mcp.md + Catalog Capability: + - specification/catalog/index.md + - specification/catalog/search.md + - specification/catalog/lookup.md + - specification/catalog/rest.md + - specification/catalog/mcp.md Order Capability: - specification/order.md Identity Linking Capability: diff --git a/source/schemas/shopping/catalog_lookup.json b/source/schemas/shopping/catalog_lookup.json new file mode 100644 index 00000000..73df3c9e --- /dev/null +++ b/source/schemas/shopping/catalog_lookup.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/catalog_lookup.json", + "name": "dev.ucp.shopping.catalog.lookup", + "version": "2026-01-11", + "title": "Catalog Lookup", + "description": "Product/variant lookup by identifier capability.", + "type": "object", + "$defs": { + "lookup_request": { + "type": "object", + "description": "Request body for catalog lookup.", + "required": ["ids"], + "properties": { + "ids": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "description": "Identifiers to lookup. Implementations MUST support product ID and variant ID; MAY support secondary identifiers (SKU, handle, etc.)." + }, + "context": { + "$ref": "types/context.json" + } + } + }, + "lookup_response": { + "type": "object", + "required": [ + "ucp", + "products" + ], + "properties": { + "ucp": { + "$ref": "../ucp.json#/$defs/response_catalog_schema" + }, + "products": { + "type": "array", + "items": { + "$ref": "types/product.json" + }, + "description": "Products matching the requested identifiers. May contain fewer items if some identifiers not found, or more if identifiers match multiple products." + }, + "messages": { + "type": "array", + "items": { + "$ref": "types/message.json" + }, + "description": "Errors, warnings, or informational messages about the requested items." + } + } + } + } +} diff --git a/source/schemas/shopping/catalog_search.json b/source/schemas/shopping/catalog_search.json new file mode 100644 index 00000000..9591ef94 --- /dev/null +++ b/source/schemas/shopping/catalog_search.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/catalog_search.json", + "name": "dev.ucp.shopping.catalog.search", + "version": "2026-01-11", + "title": "Catalog Search", + "description": "Product catalog search capability.", + "type": "object", + "$defs": { + "search_request": { + "type": "object", + "required": ["query"], + "properties": { + "query": { + "type": "string", + "description": "Free-text search query." + }, + "context": { + "$ref": "types/context.json" + }, + "filters": { + "$ref": "types/search_filters.json" + }, + "pagination": { + "$ref": "types/pagination.json#/$defs/request" + } + } + }, + "search_response": { + "type": "object", + "required": [ + "ucp", + "products" + ], + "properties": { + "ucp": { + "$ref": "../ucp.json#/$defs/response_catalog_schema" + }, + "products": { + "type": "array", + "items": { + "$ref": "types/product.json" + }, + "description": "Products 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/category.json b/source/schemas/shopping/types/category.json new file mode 100644 index 00000000..f6f4ff9c --- /dev/null +++ b/source/schemas/shopping/types/category.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/category.json", + "title": "Category", + "description": "A product category with optional taxonomy identifier.", + "type": "object", + "required": ["value"], + "properties": { + "value": { + "type": "string", + "description": "Category value or path (e.g., 'Apparel > Shirts', '1604')." + }, + "taxonomy": { + "type": "string", + "description": "Source taxonomy. Well-known values: `google_product_category`, `shopify`, `merchant`." + } + } +} diff --git a/source/schemas/shopping/types/context.json b/source/schemas/shopping/types/context.json index afd46b86..40aa0506 100644 --- a/source/schemas/shopping/types/context.json +++ b/source/schemas/shopping/types/context.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/shopping/types/context.json", "title": "Context", - "description": "Provisional buyer signals for relevance and localization: product availability, pricing, currency, tax, shipping, payment methods, and eligibility (e.g., student or affiliation discounts). Businesses SHOULD use these values when authoritative data (e.g., address) is absent, and MAY ignore unsupported values without returning errors. 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. Platforms SHOULD progressively enhance context throughout the buyer journey.", + "description": "Provisional buyer signals for relevance and localization—not authorization. Businesses SHOULD use these values when authoritative data (e.g., shipping address) is absent, and MAY ignore or down-rank them if inconsistent with stronger signals (authenticated account, fraud rules, export controls). Eligibility and policy enforcement MUST occur at checkout/order time with authoritative 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, "properties": { @@ -21,6 +21,14 @@ "intent": { "type": "string", "description": "Background context describing buyer's intent (e.g., 'looking for a gift under $50', 'need something durable for outdoor use'). Informs relevance, recommendations, and personalization." + }, + "language": { + "type": "string", + "description": "Preferred language for content. Use IETF BCP 47 language tags (e.g., 'en', 'fr-CA', 'zh-Hans'). For REST, equivalent to Accept-Language header—platforms SHOULD fall back to Accept-Language when this field is absent; when provided, overrides Accept-Language. Businesses MAY return content in a different language if unavailable." + }, + "currency": { + "type": "string", + "description": "Preferred currency (ISO 4217, e.g., 'EUR', 'USD'). Businesses determine currency from context and authoritative signals; this hint MAY inform selection in multi-currency markets. Responses include negotiated currency." } } } diff --git a/source/schemas/shopping/types/description.json b/source/schemas/shopping/types/description.json new file mode 100644 index 00000000..8de81df1 --- /dev/null +++ b/source/schemas/shopping/types/description.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/description.json", + "title": "Description", + "description": "Description content in one or more formats. At least one format must be provided.", + "type": "object", + "properties": { + "plain": { + "type": "string", + "description": "Plain text content." + }, + "html": { + "type": "string", + "description": "HTML-formatted content. Security: Platforms MUST sanitize before rendering—strip scripts, event handlers, and untrusted elements. Treat all rich text as untrusted input." + }, + "markdown": { + "type": "string", + "description": "Markdown-formatted content." + } + }, + "minProperties": 1 +} diff --git a/source/schemas/shopping/types/media.json b/source/schemas/shopping/types/media.json new file mode 100644 index 00000000..f9ad2138 --- /dev/null +++ b/source/schemas/shopping/types/media.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/media.json", + "title": "Media", + "description": "Product media item (image, video, etc.).", + "type": "object", + "required": [ + "type", + "url" + ], + "properties": { + "type": { + "type": "string", + "enum": ["image", "video", "model_3d"], + "description": "Media type discriminator." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the media resource." + }, + "alt_text": { + "type": "string", + "description": "Accessibility text describing the media." + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels (for images/video)." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels (for images/video)." + } + } +} diff --git a/source/schemas/shopping/types/option_value.json b/source/schemas/shopping/types/option_value.json new file mode 100644 index 00000000..f243b631 --- /dev/null +++ b/source/schemas/shopping/types/option_value.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/option_value.json", + "title": "Option Value", + "description": "A selectable value for a product option.", + "type": "object", + "required": [ + "label" + ], + "properties": { + "label": { + "type": "string", + "description": "Display text for this option value (e.g., 'Small', 'Blue')." + } + } +} diff --git a/source/schemas/shopping/types/pagination.json b/source/schemas/shopping/types/pagination.json new file mode 100644 index 00000000..875fde87 --- /dev/null +++ b/source/schemas/shopping/types/pagination.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/pagination.json", + "title": "Pagination", + "description": "Cursor-based pagination for list operations.", + "type": "object", + "$defs": { + "request": { + "type": "object", + "description": "Pagination parameters for requests.", + "properties": { + "cursor": { + "type": "string", + "description": "Opaque cursor from previous response." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 25, + "default": 10, + "description": "Maximum number of results to return." + } + } + }, + "response": { + "type": "object", + "description": "Pagination information in responses.", + "properties": { + "cursor": { + "type": "string", + "description": "Cursor to fetch the next page of results." + }, + "has_next_page": { + "type": "boolean", + "description": "Whether more results are available." + }, + "total_count": { + "type": "integer", + "minimum": 0, + "description": "Total number of matching items (if available)." + } + } + } + } +} diff --git a/source/schemas/shopping/types/price.json b/source/schemas/shopping/types/price.json new file mode 100644 index 00000000..51eae57a --- /dev/null +++ b/source/schemas/shopping/types/price.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/price.json", + "title": "Price", + "description": "Price with explicit currency. Amount is always in minor units (e.g., cents for USD).", + "type": "object", + "required": [ + "amount", + "currency" + ], + "properties": { + "amount": { + "type": "integer", + "description": "Amount in minor currency units (e.g., 1000 = $10.00 USD). Use 0 for free items.", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code (e.g., 'USD', 'EUR', 'GBP').", + "pattern": "^[A-Z]{3}$" + } + } +} diff --git a/source/schemas/shopping/types/price_filter.json b/source/schemas/shopping/types/price_filter.json new file mode 100644 index 00000000..a90501a9 --- /dev/null +++ b/source/schemas/shopping/types/price_filter.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/price_filter.json", + "title": "Price Filter", + "description": "Price range filter in minor currency units. Merchant determines currency from context (e.g., country=US → USD). Response prices include explicit currency.", + "type": "object", + "properties": { + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum price in minor units." + }, + "max": { + "type": "integer", + "minimum": 0, + "description": "Maximum price in minor units." + } + } +} diff --git a/source/schemas/shopping/types/price_range.json b/source/schemas/shopping/types/price_range.json new file mode 100644 index 00000000..cd574d3a --- /dev/null +++ b/source/schemas/shopping/types/price_range.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/price_range.json", + "title": "Price Range", + "description": "A price range representing minimum and maximum values (e.g., across product variants).", + "type": "object", + "required": [ + "min", + "max" + ], + "properties": { + "min": { + "$ref": "price.json", + "description": "Minimum price in the range." + }, + "max": { + "$ref": "price.json", + "description": "Maximum price in the range." + } + } +} diff --git a/source/schemas/shopping/types/product.json b/source/schemas/shopping/types/product.json new file mode 100644 index 00000000..224fb1d4 --- /dev/null +++ b/source/schemas/shopping/types/product.json @@ -0,0 +1,89 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/product.json", + "title": "Product", + "description": "A product in the catalog with variants and options.", + "type": "object", + "required": [ + "id", + "title", + "description", + "price_range", + "variants" + ], + "properties": { + "id": { + "type": "string", + "description": "Global ID (GID) uniquely identifying this product." + }, + "handle": { + "type": "string", + "description": "URL-safe slug for SEO-friendly URLs (e.g., 'blue-runner-pro'). Use id for stable API references." + }, + "title": { + "type": "string", + "description": "Product title." + }, + "description": { + "$ref": "description.json", + "description": "Product description in one or more formats." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Canonical product page URL." + }, + "category": { + "type": "array", + "items": { + "$ref": "category.json" + }, + "description": "Product categories with optional taxonomy identifiers." + }, + "price_range": { + "$ref": "price_range.json", + "description": "Price range across all variants." + }, + "list_price_range": { + "$ref": "price_range.json", + "description": "List price range before discounts (for strikethrough display)." + }, + "media": { + "type": "array", + "items": { + "$ref": "media.json" + }, + "description": "Product media (images, videos, 3D models). First item is the featured media for listings." + }, + "options": { + "type": "array", + "items": { + "$ref": "product_option.json" + }, + "description": "Product options (Size, Color, etc.)." + }, + "variants": { + "type": "array", + "items": { + "$ref": "variant.json" + }, + "minItems": 1, + "description": "Purchasable variants of this product. First item is the featured variant for listings." + }, + "rating": { + "$ref": "rating.json", + "description": "Aggregate product rating." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Product tags for categorization and search." + }, + "metadata": { + "type": "object", + "description": "Business-defined custom data extending the standard product model." + } + } +} diff --git a/source/schemas/shopping/types/product_option.json b/source/schemas/shopping/types/product_option.json new file mode 100644 index 00000000..32663ae9 --- /dev/null +++ b/source/schemas/shopping/types/product_option.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/product_option.json", + "title": "Product Option", + "description": "A product option such as size, color, or material.", + "type": "object", + "required": [ + "name", + "values" + ], + "properties": { + "name": { + "type": "string", + "description": "Option name (e.g., 'Size', 'Color')." + }, + "values": { + "type": "array", + "items": { + "$ref": "option_value.json" + }, + "minItems": 1, + "description": "Available values for this option." + } + } +} diff --git a/source/schemas/shopping/types/rating.json b/source/schemas/shopping/types/rating.json new file mode 100644 index 00000000..6b0b3b5f --- /dev/null +++ b/source/schemas/shopping/types/rating.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/rating.json", + "title": "Rating", + "description": "Product rating aggregate.", + "type": "object", + "required": [ + "value", + "scale_max" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0, + "description": "Average rating value." + }, + "scale_min": { + "type": "number", + "minimum": 0, + "default": 1, + "description": "Minimum value on the rating scale (e.g., 1 for 1-5 stars)." + }, + "scale_max": { + "type": "number", + "minimum": 1, + "description": "Maximum value on the rating scale (e.g., 5 for 5-star)." + }, + "count": { + "type": "integer", + "minimum": 0, + "description": "Number of reviews contributing to the rating." + } + } +} diff --git a/source/schemas/shopping/types/search_filters.json b/source/schemas/shopping/types/search_filters.json new file mode 100644 index 00000000..5ca06394 --- /dev/null +++ b/source/schemas/shopping/types/search_filters.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/search_filters.json", + "title": "Search Filters", + "description": "Filter criteria to narrow search results. All specified filters combine with AND logic.", + "type": "object", + "properties": { + "category": { + "type": "array", + "items": { "type": "string" }, + "description": "Filter by product categories (OR logic — matches products in any listed category). Values match against the value field in product category entries. Valid values can be discovered from the category field in search results, merchant documentation, or standard taxonomies that businesses may align with." + }, + "price": { + "$ref": "price_filter.json" + } + }, + "additionalProperties": true +} diff --git a/source/schemas/shopping/types/selected_option.json b/source/schemas/shopping/types/selected_option.json new file mode 100644 index 00000000..5c144fd1 --- /dev/null +++ b/source/schemas/shopping/types/selected_option.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/selected_option.json", + "title": "Selected Option", + "description": "A specific option selection on a variant (e.g., Size: Large).", + "type": "object", + "required": [ + "name", + "label" + ], + "properties": { + "name": { + "type": "string", + "description": "Option name (e.g., 'Size')." + }, + "label": { + "type": "string", + "description": "Selected option label (e.g., 'Large')." + } + } +} diff --git a/source/schemas/shopping/types/variant.json b/source/schemas/shopping/types/variant.json new file mode 100644 index 00000000..ba9081fb --- /dev/null +++ b/source/schemas/shopping/types/variant.json @@ -0,0 +1,115 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/variant.json", + "title": "Variant", + "description": "A purchasable variant of a product with specific option selections.", + "type": "object", + "required": [ + "id", + "title", + "description", + "price" + ], + "properties": { + "id": { + "type": "string", + "description": "Global ID (GID) uniquely identifying this variant. Used as item.id in checkout." + }, + "sku": { + "type": "string", + "description": "Business-assigned identifier for inventory and fulfillment." + }, + "barcode": { + "type": "string", + "description": "Industry-standard identifier (UPC, EAN, ISBN) for the variant." + }, + "handle": { + "type": "string", + "description": "URL-safe variant handle/slug." + }, + "title": { + "type": "string", + "description": "Variant display title (e.g., 'Blue / Large')." + }, + "description": { + "$ref": "description.json", + "description": "Variant description in one or more formats." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Canonical variant page URL." + }, + "category": { + "type": "array", + "items": { + "$ref": "category.json" + }, + "description": "Variant categories with optional taxonomy identifiers." + }, + "price": { + "$ref": "price.json", + "description": "Current selling price." + }, + "list_price": { + "$ref": "price.json", + "description": "List price before discounts (for strikethrough display)." + }, + "availability": { + "type": "object", + "description": "Variant availability for purchase.", + "properties": { + "available": { + "type": "boolean", + "description": "Whether this variant can be purchased." + } + } + }, + "selected_options": { + "type": "array", + "items": { + "$ref": "selected_option.json" + }, + "description": "Option selections that define this variant." + }, + "media": { + "type": "array", + "items": { + "$ref": "media.json" + }, + "description": "Variant media (images, videos, 3D models). First item is the featured media for listings." + }, + "rating": { + "$ref": "rating.json", + "description": "Variant rating." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Variant tags for categorization and search." + }, + "metadata": { + "type": "object", + "description": "Business-defined custom data extending the standard variant model." + }, + "seller": { + "type": "object", + "description": "Optional seller context for this variant.", + "properties": { + "name": { + "type": "string", + "description": "Seller display name." + }, + "links": { + "type": "array", + "items": { + "$ref": "link.json" + }, + "description": "Seller policy and information links." + } + } + } + } +} diff --git a/source/schemas/ucp.json b/source/schemas/ucp.json index d6cffac8..4a703dc4 100644 --- a/source/schemas/ucp.json +++ b/source/schemas/ucp.json @@ -206,6 +206,23 @@ } } ] + }, + + "response_catalog_schema": { + "title": "UCP Catalog Response Schema", + "description": "UCP metadata for catalog responses.", + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "capabilities": { + "additionalProperties": { + "items": { "$ref": "capability.json#/$defs/response_schema" } + } + } + } + } + ] } } } diff --git a/source/services/shopping/openapi.json b/source/services/shopping/openapi.json index 54282406..439a3e2e 100644 --- a/source/services/shopping/openapi.json +++ b/source/services/shopping/openapi.json @@ -506,6 +506,80 @@ } } } + }, + "/catalog/search": { + "post": { + "operationId": "search_catalog", + "summary": "Search Catalog", + "description": "Search for products in the business's catalog.", + "parameters": [ + { "$ref": "#/components/parameters/authorization" }, + { "$ref": "#/components/parameters/x_api_key" }, + { "$ref": "#/components/parameters/request_signature" }, + { "$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/catalog_search_request" } + } + } + }, + "responses": { + "200": { + "description": "Search results", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/catalog_search_response" } + } + } + } + } + } + }, + "/catalog/lookup": { + "post": { + "operationId": "lookup_catalog", + "summary": "Batch Lookup Catalog Items", + "description": "Lookup one or more products by ID with explicit market context. Supports batch lookups of multiple items in a single request.", + "parameters": [ + { "$ref": "#/components/parameters/authorization" }, + { "$ref": "#/components/parameters/x_api_key" }, + { "$ref": "#/components/parameters/request_signature" }, + { "$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/catalog_lookup_request" } + } + } + }, + "responses": { + "200": { + "description": "Lookup result", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/catalog_lookup_response" } + } + } + } + } + } } }, "webhooks": { @@ -712,6 +786,18 @@ }, "ucp": { "$ref": "../../schemas/ucp.json" + }, + "catalog_search_request": { + "$ref": "../../schemas/shopping/catalog_search.json#/$defs/search_request" + }, + "catalog_search_response": { + "$ref": "../../schemas/shopping/catalog_search.json#/$defs/search_response" + }, + "catalog_lookup_request": { + "$ref": "../../schemas/shopping/catalog_lookup.json#/$defs/lookup_request" + }, + "catalog_lookup_response": { + "$ref": "../../schemas/shopping/catalog_lookup.json#/$defs/lookup_response" } } } diff --git a/source/services/shopping/openrpc.json b/source/services/shopping/openrpc.json index fc061701..df30a25e 100644 --- a/source/services/shopping/openrpc.json +++ b/source/services/shopping/openrpc.json @@ -244,6 +244,48 @@ "name": "cart", "schema": {"$ref": "../../schemas/shopping/cart.json"} } + }, + { + "name": "search_catalog", + "summary": "Search for products in the catalog", + "description": "Search for products using query text, filters, and pagination.", + "params": [ + { + "name": "meta", + "required": true, + "schema": {"$ref": "#/components/schemas/meta"} + }, + { + "name": "catalog", + "required": true, + "schema": {"$ref": "../../schemas/shopping/catalog_search.json#/$defs/search_request"} + } + ], + "result": { + "name": "response", + "schema": {"$ref": "../../schemas/shopping/catalog_search.json#/$defs/search_response"} + } + }, + { + "name": "lookup_catalog", + "summary": "Batch lookup products or variants by identifier", + "description": "Batch lookup of products or variants by identifier. See Catalog Lookup capability for supported identifier types and resolution behavior.", + "params": [ + { + "name": "meta", + "required": true, + "schema": {"$ref": "#/components/schemas/meta"} + }, + { + "name": "catalog", + "required": true, + "schema": {"$ref": "../../schemas/shopping/catalog_lookup.json#/$defs/lookup_request"} + } + ], + "result": { + "name": "response", + "schema": {"$ref": "../../schemas/shopping/catalog_lookup.json#/$defs/lookup_response"} + } } ] }