diff --git a/docs/specification/overview/index.md b/docs/specification/overview/index.md index df8bac3a6..54aa6028e 100644 --- a/docs/specification/overview/index.md +++ b/docs/specification/overview/index.md @@ -31,3042 +31,3042 @@ Schema notes: unless otherwise specified - Amounts format: Minor units (cents) -## Quantities and units +## Discovery, Governance, and Negotiation -UCP uses a shared quantity representation wherever a schema contains an integer -`quantity`, a `quantity_unit`, or the shared measure type. +UCP separates [protocol version selection](#protocol-version) from +[capability negotiation](#capability-versions). A Business advertises its current +protocol version and links to profiles for older supported versions. After the +Platform selects one exact version, the Business determines the active +capabilities from the versions both parties advertise. Version lifecycle, +including when to remove an older version, is a Business policy decision; UCP +does not prescribe a deprecation schedule. Business and Platform profiles can be +cached by both parties. -A `quantity` is an integer count of **steps**. A unit descriptor consists of: +### Namespace Governance -- `unit` — a required, stable machine identifier. -- `scale` — an optional nonnegative integer, at most 15 (a bound derived from - the integer range; see below). Its effective value is the provided value or - `0` when omitted. -- `display_text` — a required printable label for the unit. +UCP uses reverse-domain naming to encode governance authority directly into +capability identifiers. This eliminates the need for a central registry. -One step is `10^-scale` of `unit`. The shared measure type adds a required -integer `value`, which is also a count of those steps. Because these counts are -integers, `scale` fixes the representation's granularity. A unit descriptor's -machine identity is the (`unit`, effective `scale`) pair; `display_text` is not -part of that identity. This identity applies only to the unit descriptor; it -does not identify the purchasable item or exhaustively describe one sale unit. +#### Naming Convention -The default sale basis is `each`, with machine identity (`C62`, `0`). `C62` is -the United Nations Centre for Trade Facilitation and Electronic Business -(UN/CEFACT) Recommendation 20 (Rec20) Common Code for one/each. The Business -**MAY** omit `quantity_unit` from an authoritative Business representation to -encode this default. When a Business or Platform includes a -descriptor whose `unit` is `C62`, it **MUST** use an effective `scale` of `0`; -`scale` can only be omitted or explicitly set to `0`. +All capability and service names **MUST** use the format: -UCP does not put floating-point numbers on the wire. Quantity arithmetic -feeds money — `price × quantity × 10^-scale` prices a line, `fulfilled` -accumulates across fulfillment events, and status derives from -`fulfilled == total` — so quantities get money's representation: an integer -count plus a declared interpretation, exactly as an `amount` relates to its -`currency`. Integer counts keep every total and comparison exact in every -language, and UCP therefore defines no rounding tolerances and no epsilon -comparisons anywhere in the quantity lifecycle. A fulfilled quantity that -legitimately differs from the ordered quantity — a 1.90 lb pick against a -2.00 lb order — is a commercial fact reconciled through -[adjustments](../shopping/order/index.md#adjustments) that move money together with quantity, -not a numeric error absorbed by comparison fuzz. +```text +{reverse-domain}.{service}.{capability} +``` -Reading a quantity requires no arithmetic and no unit knowledge: shift the -decimal point `scale` places and append `display_text`. `150` with -`{ "scale": 2, "display_text": "kg" }` renders as `1.50 kg`, by the same code -path for a Rec20 code and for a custom unit. Unlike a currency exponent, -`scale` is per-item data rather than a static table — which is why -authoritative responses always carry their own descriptor on every non-`each` -line. +**Components:** -### Integer range and ingestion +- `{reverse-domain}` - Authority identifier derived from domain ownership +- `{service}` - Service/vertical category (e.g., `shopping`, `common`) +- `{capability}` - The specific capability name -Every integer-valued field in UCP — amounts, quantity step counts, measure -values — is a JSON integer; each field's schema declares its sign and -bounds, and all are capped at ±(2^53 − 1) (±9,007,199,254,740,991) — the -range within which every JSON implementation agrees exactly on integer -values -([RFC 8259](https://www.rfc-editor.org/rfc/rfc8259.html){ target="_blank" }, -Section 6) and within which -[JCS](https://www.rfc-editor.org/rfc/rfc8785.html){ target="_blank" } -canonicalization, required for -[AP2 mandate signing](../payment/extensions/ap2-mandates.md#canonicalization), is defined. The -same cap derives `scale`'s maximum of 15: at scale 16, one whole unit -(10^16 steps) would be unrepresentable. An out-of-range value is -schema-invalid and is rejected like any other invalid payload. +**Examples:** -Arithmetic over these values **MUST** be exact. Within the wire range, -IEEE 754 binary64 — a JavaScript `Number` from `JSON.parse` — holds every -integer exactly; products such as `amount × quantity` can exceed 64 bits, so -use wider or arbitrary-precision integers, or overflow checks. An -implementation that cannot produce an exact, in-range result **MUST** -surface an error rather than emit, display, or act on an approximate or -wrapped value. +| Name | Authority | Service | Capability | +| ----------------------------------- | ----------- | -------- | ---------------- | +| `dev.ucp.shopping.checkout` | ucp.dev | shopping | checkout | +| `dev.ucp.shopping.fulfillment` | ucp.dev | shopping | fulfillment | +| `dev.ucp.common.identity_linking` | ucp.dev | common | identity_linking | +| `com.example.payments.installments` | example.com | payments | installments | -When UCP data crosses into external systems, implementations **SHOULD** -convert once at ingestion — apply the declared scale (or currency exponent) -into an exact decimal type (SQL `NUMERIC`, Java `BigDecimal`, Python -`Decimal`), or carry the (value, scale) pair unchanged — and **SHOULD NOT** -perform scale application or value-bearing arithmetic in binary floating -point. +#### Authority Binding -### Unit vocabulary +Reverse-domain names serve two purposes: collision-safe **identifiers** (keys +and references), and **entities** — capabilities, services, and payment +handlers — that declare a fetched `schema` URL describing them. Authority +binding applies to every entity with a remote `schema`: a declared `schema` +URL's origin **MUST** match the namespace authority in its name. -The Business **SHOULD** use the exact Rec20 Common Code unless no code -accurately identifies the unit. When no Rec20 code accurately identifies the -unit, the Business **MAY** use a custom unit identifier. If it does, the Business -**MUST** use that identifier consistently for the same unit. The Platform -**MUST** treat an unrecognized `unit` value as opaque. The following table is -non-exhaustive: +A capability **MUST** declare a `schema`; services and payment handlers declare +one where their transport or handler defines it. Each entity **MAY** also +declare a `spec` URL (human-readable documentation). -| Code | Unit | -| :---- | :----------- | -| `C62` | one / `each` | -| `KGM` | kilogram | -| `GRM` | gram | -| `LBR` | pound | -| `MLT` | millilitre | -| `LTR` | litre | -| `MTR` | metre | -| `INH` | inch | -| `YRD` | yard | -| `FTK` | square foot | -| `MTK` | square metre | -| `HUR` | hour | -| `MIN` | minute | +This binding guarantees **provenance, not trust**: a valid binding proves only +that the reverse-domain name is controlled by the party that owns the +corresponding domain — an entity cannot be published under a namespace its +author does not control. It does **not** assert that the entity is trustworthy, +correct, or worth supporting. Whether to negotiate, trust, or implement it is +always the client's decision; this binding only tells the client *who* is making +the claim. Provenance is established from domain ownership and evaluated at +negotiation time. -Rec20 includes X-prefixed package units derived from UN/CEFACT Recommendation -21 (Rec21). UCP deliberately excludes those values from `quantity_unit`. The -Business **MUST** make package form part of the purchasable variant's identity -and count packages as `each`. The Business **MUST NOT** use an X-prefixed -Rec21-derived package code as `quantity_unit`. +The `spec` URL is documentation, not part of the machine trust path, so its +origin is **not** authority-bound: it **MUST** be `https` but **MAY** be served +from any host (e.g. a docs subdomain or third-party docs host). Only the +`schema` URL carries the authority binding defined below. -### Display text +##### Derivation algorithm -When sending a unit descriptor, a Business or Platform **MUST** include -`display_text`. The Platform **MUST** use that value when it does not recognize -`unit`. For a recognized Rec20 code, the Platform **MAY** substitute its own -localized label. The Business and Platform **MUST NOT** use `display_text` when -matching machine identities or as an input to quantity conversion. +The authority is derived **from the `schema` URL host** — which names the +owning domain directly, with no ambiguity about where the domain ends — and +validated as a label-aligned prefix of, or an exact match for, the entity's +name. For the `schema` URL of an entity whose name is `name`, a platform +**MUST** apply the following: -### Ordering increment +1. Parse the URL with a conformant (WHATWG) URL parser. It **MUST** parse, + **MUST** use the `https` scheme, and **MUST NOT** contain userinfo (a + `user:pass@` component). Substring matching on the raw URL is **NOT** + permitted — e.g. `https://ucp.dev@evil.example/x.json` has host + `evil.example`, not `ucp.dev`. +2. The host **MUST** be a registered domain name of at least two labels. + IP-literal hosts (`https://203.0.113.10/...`) and single-label hosts + (`https://localhost/...`) are invalid authorities. +3. Take the URL's hostname (the host without any port), normalize it (lowercase; + strip a trailing `.`; internationalized domains in A-label / punycode form), + and **reverse its labels** to form the `authority_prefix` (host `ucp.dev` → + `dev.ucp`). +4. The binding is valid if and only if **either** of the following holds: + - **Exact match** — `name` equals `authority_prefix`. The name is itself the + reversed host, so the publisher demonstrably controls the entire namespace. + This is the shape for an entity whose identity is a bare controlled domain, + such as a payment handler `com.example.pay` served from `pay.example.com` + (reversed host `com.example.pay` equals the name). + - **Prefixed** — `name` is `authority_prefix`, then a `.`, then one or more + further labels; that is, the character immediately after `authority_prefix` + in `name` is a `.`. Requiring that separating `.` keeps the match on a + label boundary — it stops `com.example` (host `example.com`) from matching + a neighboring namespace like `com.examplecorp.*`, where `com.example` is a + textual prefix but not a label-aligned one. -A sale-basis descriptor (`quantity_unit`) **MAY** declare an `increment`: an -optional positive integer, denominated in steps, whose effective value is the -provided value or `1` when omitted. Only the sale basis carries an increment; -the bare unit descriptor and the shared measure type do not. It declares the -ordering granularity the Business sells in — for example, an item sold by the -pound with `scale` `2` and `increment` `25` is sold in 0.25 lb multiples. +Authority binding establishes **provenance only** — that the name is controlled +by the party serving its `schema`. It does **not** require any label beyond the +authority itself. The `{reverse-domain}.{service}.{capability}` shape is a +separate [Naming Convention](#naming-convention) that governs capability and +service names — it does not apply to payment handlers — and is validated +independently of this check. -`scale` and `increment` play different roles: `scale` bounds what any quantity -can express; `increment` shapes what the Platform asks for. The increment is -advisory merchandising policy, not a representational bound — Platform-authored -quantities **SHOULD** be integer multiples of the line's effective increment, -while Business-authored quantities (checkout revisions, fulfillment events, -adjustments) are bounded only by `scale`. `increment` is not part of the -unit-descriptor machine identity and **MUST NOT** participate in mismatch -comparison. +Any labels after the authority prefix are treated as opaque by this check; they +are not inspected or split. -Request assertions, mismatch handling, response echo, off-increment request -handling, pricing, and lifecycle behavior are defined by the capability that -uses the shared representation. +| Entity name | `schema` host | `authority_prefix` | Result | +| ----------------------------------- | ------------------ | ------------------ | ------------------- | +| `dev.ucp.shopping.checkout` | `ucp.dev` | `dev.ucp` | **accept** (prefix) | +| `dev.ucp.shopping.checkout` | `shopping.ucp.dev` | `dev.ucp.shopping` | **accept** (prefix) | +| `com.example.payments.installments` | `example.com` | `com.example` | **accept** (prefix) | +| `com.example.pay` | `pay.example.com` | `com.example.pay` | **accept** (exact) | +| `com.example.pay` | `example.com` | `com.example` | **accept** (prefix) | +| `com.example.pay` | `evil.example` | `example.evil` | **reject** | +| `dev.ucp.shopping.checkout` | `evil.example` | `example.evil` | **reject** | +| `com.examplecorp.pay` | `example.com` | `com.example` | **reject** | +| `com.example.pay` | `cdn.example.com` | `com.example.cdn` | **reject** | -## Request Constraints +An entity's `schema` is served from a host whose reversed labels either **equal** +its name or are a **label-aligned prefix** of it. A host whose reversed labels +are exactly the name (`pay.example.com` for `com.example.pay`) satisfies the +exact case; a canonical apex host (`example.com` for `com.example.*`) satisfies +the prefix case; a subdomain satisfies the prefix case only when its labels line +up with the namespace path (`shopping.ucp.dev` for `dev.ucp.shopping.*`). Because +a parent domain's reversed labels are also a prefix, a name such as +`com.example.pay` binds equally from its exact host (`pay.example.com`) or a +parent authority (`example.com`) — both prove control. Unrelated subdomains such +as a shared CDN do **not** satisfy any case — host the canonical schema on a +name-aligned origin. -After capabilities and extensions are negotiated, the resolved UCP request -schema defines the fields and structure allowed for an operation. In an -authoritative response, a Business can use `ucp.request_constraints` to signal -additional rules it will apply when evaluating request data in the next -Platform request. For example, it can constrain a Line Item quantity to exactly -`100` sale-basis steps or require a submitted payment instrument to include -`billing_address`. A Platform can evaluate these constraints before submission, -avoiding a round trip for request data the Business has already indicated it -will reject. +The check uses the `schema` URL host directly and does not consult the +[Public Suffix List](https://publicsuffix.org/), so it treats a **public +suffix** — a domain under which independent parties can register names, from +`co.uk` to the list's private-section suffixes operated by services that let +third parties register subdomains or buckets (`github.io`, object storage, app +platforms) — as an ordinary authority. Co-tenants under such a suffix satisfy +the same prefix, so declare entities only under a **registrable domain** (a +public suffix plus one label) that you exclusively control. -`ucp.request_constraints` is used only in authoritative operation responses and -has no effect in discovery profiles or operation requests. +##### Enforcement -### Validation model +A platform **MUST** validate each business-declared `schema` URL before fetching +it. If the URL's origin does not match the entity's namespace authority (per +[Derivation algorithm](#derivation-algorithm)), the platform **MUST NOT** fetch +it and **MUST** reject the entity — treated as not present and never +activated. A `spec` URL **MUST** be a valid `https` URL. A platform **MUST NOT** follow redirects (`3xx`) when fetching a `schema` URL, consistent with profile fetches. -A submitted request is valid under Request Constraints only if it satisfies the -resolved request schema and every object selected by Request Constraints -satisfies the corresponding Constraint Expression. Request Constraints only -narrow the resolved request schema; they cannot make a request valid when that -schema rejects it. A Business evaluates the request as follows: - -```text -valid = validate(resolved_request_schema, request) - -for each constraint: - objects = select(request, effective_path(constraint)) - valid = valid AND validate_all( - constraint_expression(constraint), - objects - ) +The platform fetches and composes business-declared schemas to validate every +request and response, so validating the binding ensures each composed schema is +sourced from the party that owns the entity's namespace. A business **SHOULD** +apply the same check to the platform profile and exclude any entity whose +binding fails. -return valid -``` +Binding validates the declared hostname for provenance; it is **not** a +fetch-safety control and does not authorize dereferencing. Fetching the `schema` +URL — like any URL fetched during discovery — is additionally subject to the +protocol's URL fetch-safety requirements, which guard the *resolved* address +(not just the hostname) against server-side request forgery toward special-use +or cloud-metadata addresses and DNS rebinding. The hostname check and the +resolved-address check are independent, and both apply. -A Platform **MAY** perform the same checks as preflight. For each chosen value, -the Platform **MUST** use its effective path and complete Constraint Expression. -The Platform **MAY** submit the request regardless of the preflight result; -Business evaluation is authoritative. +#### Governance Model -### Constraint Expression +| Namespace Pattern | Authority | Governance | +| ----------------- | ------------ | ------------------- | +| `dev.ucp.*` | ucp.dev | UCP governing body | +| `com.{vendor}.*` | {vendor}.com | Vendor organization | +| `org.{org}.*` | {org}.org | Organization | -A `request_constraints` value and every nested constraint object use embedded -JSON Schema Draft 2020-12 language. The outer value may additionally contain an -optional `path`; nested constraint objects may not. The grammar does not admit -`ucp`. Keys in `properties` name fields on selected request objects. +The `dev.ucp.*` namespace is reserved for capabilities sanctioned by the UCP +governing body. Vendors **MUST** use their own reverse-domain namespace for +custom capabilities. -The constraint begins at an Object Constraint. Object Constraints may nest -through `properties` and `anyOf`; Value Constraints occur only as values in an -Object Constraint's `properties` map. +### Services -| Position | Admitted members | Shape and behavior | -| :-- | :-- | :-- | -| Object Constraint | `required`, `properties`, `anyOf` | `required` is an array of unique field names. `properties` maps field names to Object or Value Constraints. `anyOf` is a non-empty array of non-empty Object Constraints, at least one of which the object must satisfy. An empty Object Constraint is a valid no-op at every Object Constraint position except an `anyOf` branch. | -| Value Constraint | `enum`, `const` | `enum` is a non-empty array of unique JSON values. `const` is any JSON value. At least one member is present; when both are present, both apply. | +A **service** defines the API surface for a vertical (shopping, common, etc.). +Services include operations, events, and transport bindings defined via +standard formats: -No other member is admitted at either grammar position. +- **REST**: OpenAPI 3.x (JSON format) +- **MCP**: OpenRPC (JSON format) +- **A2A**: Agent Card Specification +- **EP(embedded)**: OpenRPC (JSON format) -Members present at the same Object Constraint all apply. `anyOf` does not -narrow, override, or replace its siblings; the object must satisfy every -sibling member and at least one branch. Each branch is an ordinary Object -Constraint and cannot carry `path`, so every branch is evaluated against the -same selected object. +A service is identified by its reverse-domain registry key (e.g., +`dev.ucp.shopping`). In a profile, services are keyed by that name, and each +entry in `services[name][]` pairs the service with one transport binding and +declares the service `version`: in release `D` that version is `D`. This is the +service version, not a transport version — the binding has no separate version. +The OpenAPI or OpenRPC artifact a binding references carries its own +`info.version` as release metadata, not a separate version to negotiate. See +[Component Versioning and Release Snapshots](#component-versioning-and-release-snapshots). -Branches are alternatives, not a partition: an object satisfying more than one -branch is valid. Within a branch, `properties` constrains a member only when -that member is present, so a branch pinning a discriminator through -`properties` alone is also satisfied by an object that omits it; naming the -discriminator in the branch's `required` makes the branch match only the shape -it describes. +#### Service Definition -The grammar is defined independently of the object it is bound to. Request -Constraints bind it to objects in the next request and add `path`; other UCP -schemas reuse it where a declaration already identifies the object it -constrains, such as -[`available_instruments[].constraints`](site:schemas/common/types/available_payment_instrument.json), -whose object is the `constraint_target` declared by the instrument schema for -that entry's `type`. +{{ extension_schema_fields('service.json#/$defs/platform_schema', 'overview') }} -### Path +Transport definitions **MUST** be thin: they declare method names and reference +base schemas only. See [Requirements](#requirements) for details. -Every `request_constraints` value has exactly one effective path. When `path` -is omitted, the effective path is the -[RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html){ target="_blank" } -Normalized Path from the authoritative response root to the structured response -object whose `ucp` member contains `request_constraints`. When the Business -provides `path`, that value becomes the effective path and supersedes the -Normalized Path that would otherwise be derived; the Business **MUST** make it -a complete RFC 9535 JSONPath query. In both cases, the effective path is -evaluated against the next logical UCP request to that resource. The effective -path therefore differs from response-targeting paths elsewhere in UCP, such as -`messages[].path`, which are evaluated against the response that carries them. +#### Endpoint Resolution -An omitted path establishes positional correspondence for the next request, not -stable identity: an array index still identifies that position if items reorder -before that request. A Business **MUST** provide an explicit `path` when the -Normalized Path of that structured response object does not identify the -intended request objects. When a constraint must follow a stable identity into -the next request, the Business **MUST** use an explicit query that encodes that -association. +The `endpoint` field provides the base URL for API calls. OpenAPI paths are +appended to this endpoint to form the complete URL. -A Business that constructs a query from data **MUST** serialize and escape each -dynamic value as a valid RFC 9535 literal and **MUST NOT** use unsafe string -concatenation. +**Example:** -Before emitting a value, a Business **MUST** validate the complete value against -the shared -[Request Constraints](site:schemas/common/types/request_constraints.json) -schema, and a Platform **MUST** do the same before using that value for -preflight; `path` selects the objects and the remaining members form the -[Constraint Expression](site:schemas/common/types/constraint_expression.json). + +```json +{ + "version": "{{ ucp_version }}", + "transport": "rest", + "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/rest.openapi.json", + "endpoint": "https://business.example.com/api/v2" +} +``` -A constraint is not tied to an operation name; it applies to every object its -path selects. A path that selects zero objects has no effect. When -`request_constraints` is in the response root's `ucp` member and `path` is -omitted, the effective path is `$`. +With OpenAPI path `/checkout-sessions`, the resolved URL is: -If multiple paths select the same object, every corresponding Constraint -Expression applies. The shared Request Constraints schema validates each value -independently and cannot guarantee that expressions across overlapping paths can -all be satisfied. A Business **MUST** ensure expressions that can apply to the -same object are jointly satisfiable. Contradictory requirements on the same -property are a Business authoring error. +```text +POST https://business.example.com/api/v2/checkout-sessions +``` -A Platform that performs preflight on overlapping values evaluates every value -it chose; if any chosen value fails for the concrete request, preflight fails. -There is no precedence or override. +**Rules:** -### Guidelines +- `endpoint` **MUST** be a valid URL with scheme (https) +- `endpoint` **SHOULD NOT** have a trailing slash +- OpenAPI paths are relative and appended directly to endpoint +- Same resolution applies to MCP endpoints for JSON-RPC calls +- `endpoint` for A2A transport refers to the Agent Card URL for the agent -#### Business +### Capabilities -A Business **MAY** include `ucp.request_constraints` in an authoritative -response to describe rules it will enforce against request data in the next -Platform request. The Business **MUST** emit Request Constraints that conform to -the shared Request Constraints schema, **MUST** use valid effective paths that -select only objects, and **MUST** enforce every constraint it emits against that -next request. +A **capability** is a feature within a service. It declares what +functionality is supported and where to find documentation and schemas. -A Business **SHOULD NOT** emit Request Constraints for rules that may change -before the next request unless it can continue to enforce the advertised -constraint. Execution-time conditions such as inventory availability, fraud -decisions, and payment authorization remain governed by the operation's existing -outcomes and `messages`. +#### Capability Definition -#### Platform +{{ extension_schema_fields('capability.json#/$defs/platform_schema', 'overview') }} -A Platform **MAY** use `ucp.request_constraints` for preflight before submitting -request data. It **MAY** evaluate any supported subset of values. This preflight -is optional and advisory. +#### Extensions -A malformed or unsupported value, a value whose path selects a non-object, or a -value the Platform cannot evaluate within its resource limits is unavailable -for preflight, not a pass or failure. A Platform performing preflight **MUST** -skip the whole unavailable value and continue with any other chosen values. If -complete evaluation finds a violation, that value fails preflight. +An **extension** is an optional module that augments another capability. +Extensions use the `extends` field to declare their parent(s): -A successful preflight result covers only the values the Platform evaluated. -UCP defines no new wire status or issue-marker field for preflight results. + +```json +{ + "dev.ucp.shopping.fulfillment": [ + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/shopping/extensions/fulfillment", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/fulfillment.json", + "extends": "dev.ucp.shopping.checkout" + } + ] +} +``` -### Scope and lifecycle +##### Multi-Parent Extensions -Each authoritative resource response from the Business supplies Request -Constraints for the next request to that resource. The authoritative response -to that request supplies the constraints for the following request and replaces -the prior set. Omission clears the set. Invalid values in the new set do not -preserve stale values, and sets are not merged by `path`. +Extensions **MAY** extend multiple parent capabilities by using an array: -Only `request_constraints` values attached to eligible structured response -objects under the [Reserved `ucp` Member](#the-ucp-protocol-namespace) rules -make up the set; dictionary keys are data. + +```json +{ + "dev.ucp.shopping.discount": [ + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/shopping/extensions/discount", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/discount.json", + "extends": ["dev.ucp.shopping.checkout", "dev.ucp.shopping.cart"] + } + ] +} +``` -A response without an authoritative resource supplies no Request Constraints -set for a following request. A response containing an authoritative resource -supplies the set even when it reports an application error. A partial -authoritative resource representation supplies a set only when the resource -contract defines its scope. +When an extension declares multiple parents: -### Operation outcomes +- The extension **MAY** define different fields for each capability it extends + (e.g., `loyalty_earned` for checkout, `loyalty_preview` for cart) +- See [Intersection Algorithm](#intersection-algorithm) for negotiation rules -Request Constraints provide proactive, machine-evaluable preflight for the next -request; `messages` report outcomes from a submitted request, including runtime -outcomes. Passing validation against both the resolved request schema and -Request Constraints establishes only schema validity; the request can still -fail other Business rules. The containing operation's existing semantics and -outcome/error contract, including `messages`, continue to govern -submitted-request and runtime outcomes. Request Constraints add no outcome or -error code. +Extensions can be: -### Examples +- **Official**: `dev.ucp.shopping.fulfillment` extends `dev.ucp.shopping.checkout` +- **Vendor**: `com.example.installments` extends `dev.ucp.shopping.checkout` -#### Basket-wide and targeted quantities +### Schema Composition -The following Cart responses are alternatives that illustrate different scopes. +Extensions can add new fields and modify shared structures (e.g., discounts +modify `totals`, fulfillment adds fulfillment to `totals.type`). -=== "Basket-wide" +#### Requirements - - ```json - { - "ucp": { - "version": "{{ ucp_version }}", - "request_constraints": { - "path": "$['line_items'][*]", - "properties": { - "quantity": {"const": 1} - } - } - }, - "id": "cart_123", - "line_items": [ - { - "id": "line_123", - "item": { - "id": "sku_123", - "title": "Bulk screws", - "price": 1200 - }, - "quantity": 1, - "totals": [ - {"type": "subtotal", "amount": 1200}, - {"type": "total", "amount": 1200} - ] - } - ], - "currency": "USD", - "totals": [ - {"type": "subtotal", "amount": 1200}, - {"type": "total", "amount": 1200} - ] - } - ``` - -=== "Targeted" - - - ```json - { - "ucp": { - "version": "{{ ucp_version }}" - }, - "id": "cart_123", - "line_items": [ - { - "id": "line_123", - "item": { - "id": "sku_123", - "title": "Bulk screws", - "price": 1200 - }, - "quantity": 100, - "totals": [ - {"type": "subtotal", "amount": 120000}, - {"type": "total", "amount": 120000} - ], - "ucp": { - "request_constraints": { - "path": "$['line_items'][?@['id'] == 'line_123']", - "properties": { - "quantity": {"const": 100} - } - } - } - } - ], - "currency": "USD", - "totals": [ - {"type": "subtotal", "amount": 120000}, - {"type": "total", "amount": 120000} - ] - } - ``` - -The basket-wide response emits a root constraint that applies quantity `1` to -every Line Item in the next request. The targeted response uses ambient local -authoring and a stable-ID path to apply quantity `100` only to the Line Item -whose `id` is `line_123`; stable-ID rebinding survives reorder. If that path -finds no match, it selects zero objects and has no effect. These are separate -responses and alternatives. Combining them as written would create -contradictory constraints for `line_123` and is a Business authoring error. +- Transport definitions (OpenAPI/OpenRPC) **MUST** reference base schemas + only. They **MUST NOT** enumerate fields or define payload shapes inline. +- Extensions **MUST** be self-describing. Each extension schema **MUST** + declare the types it introduces and how it modifies base types using `allOf` + composition. +- Platforms **MUST** resolve schemas client-side by fetching and composing + base schemas with active extension schemas. -#### Locked negotiated discount codes +#### Extension Schema Pattern -This Checkout response has the Discount extension active and advertises Request -Constraints for the next Checkout Update request. Because `request_constraints` -is in the Checkout response root's `ucp`, omitting `path` derives that -structured response object's Normalized Path. For this root placement, the -derived path is `$`, which selects the next request root. +Extension schemas define composed types using `allOf`. The `$defs` key **MUST** +use the full parent capability name (reverse-domain format) to enable +deterministic schema resolution: - + ```json { - "ucp": { - "version": "{{ ucp_version }}", - "status": "success", - "capabilities": { - "dev.ucp.shopping.checkout": [ - {"version": "{{ ucp_version }}"} - ], - "dev.ucp.shopping.discount": [ - {"version": "{{ ucp_version }}"} - ] - }, - "payment_handlers": {}, - "request_constraints": { - "required": ["discounts"], - "properties": { - "discounts": { - "required": ["codes"], + "$defs": { + "discounts_object": { ... }, + "dev.ucp.shopping.checkout": { + "title": "Checkout with Discount", + "allOf": [ + {"$ref": "checkout.json"}, + { + "type": "object", "properties": { - "codes": {"const": ["ACME-X7Q9-L2M4"]} + "discounts": { + "$ref": "#/$defs/discounts_object" + } } } - } - } - }, - "id": "checkout_123", - "status": "incomplete", - "currency": "USD", - "line_items": [ - { - "id": "line_123", - "item": { - "id": "sku_123", - "title": "Bulk screws", - "price": 1200 - }, - "quantity": 24, - "totals": [ - {"type": "subtotal", "amount": 28800}, - {"type": "total", "amount": 28800} ] } - ], - "totals": [ - {"type": "subtotal", "amount": 28800}, - {"type": "total", "amount": 28800} - ], - "links": [ - { - "type": "terms_of_service", - "url": "https://business.example/terms" - } - ], - "discounts": { - "codes": ["ACME-X7Q9-L2M4"] } } ``` -The next Checkout Update request is valid only if it satisfies the resolved -request schema, contains `discounts.codes`, and supplies exactly -`["ACME-X7Q9-L2M4"]`. +**Requirements:** -#### Billing address on a submitted card instrument +- Extension schemas **MUST** have a `$defs` entry for each parent declared in + `extends` +- The `$defs` key **MUST** match the parent's full capability name exactly -In this example, a Business emits `ucp.request_constraints` on an available card -instrument to require `billing_address` in the next request if it contains a -matching submitted card instrument: +This convention ensures: - +- **Self-documenting**: The schema declares exactly which parents it extends +- **Deterministic resolution**: The `extends` value maps directly to the `$defs` key +- **Verifiable**: Build-time checks can confirm each `extends` entry has a + matching `$defs` key + +##### Version Requirements + +Extension authors **SHOULD** declare a `requires` object in the extension +schema (alongside its `name`, `title`, and `description`) stating the versions +the extension depends on. A third-party extension schema declares its own +author-controlled `version`, which advances independently of `ucp.version`; +a UCP-authored `dev.ucp.*` extension declares version `D` in release `D`. +`requires.protocol` constrains the selected `ucp.version`, and +`requires.capabilities` constrains the selected versions of the named +capabilities: + + ```json { - "type": "card", - "ucp": { - "request_constraints": { - "path": "$['payment']['instruments'][?@['handler_id'] == 'processor_1' && @['type'] == 'card']", - "required": ["billing_address"] + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://acme.com/ucp/schemas/loyalty.json", + "name": "com.acme.shopping.loyalty", + "version": "2026-06-15", + "title": "Acme Loyalty Points", + "requires": { + "protocol": { "min": "2026-01-23" }, + "capabilities": { + "dev.ucp.shopping.checkout": { "min": "2026-06-01" } } + }, + "$defs": { + "dev.ucp.shopping.checkout": { ... } } } ``` -This fragment assumes its containing authoritative response payment-handler -declaration has `id: processor_1`. The explicit `path` crosses from the -response's `available_instruments[]` shape to submitted `payment.instruments[]` -and matches instruments by `handler_id` and `type`. If the next request contains -a match, the constraint requires `billing_address` on every matching instrument. -The payment-handler or instrument contract defines any stronger association. -This example does not define card brands, credentials, support, availability, or -payment policy. - -#### Alternative verification requirements on a submitted credential +The schema author — not the profile publisher — declares version +requirements. The profile publisher selects and advertises compatible +versions in their profile. -A Business accepts more than one credential shape and requires different -verification data for each. In this example, a PAN must carry a `cvc`, and a -network token must carry a `cryptogram` with its `eci_value`. Each credential -family is its own schema, so every branch discriminates on the credential's own -`type` and no rule has to branch on a sibling field: +Each constraint is an object with a required `min` (inclusive) and +optional `max` (inclusive) version. When `max` is absent, there is +no upper bound: - + ```json -{ - "type": "card", - "ucp": { - "request_constraints": { - "path": "$['payment']['instruments'][?@['handler_id'] == 'processor_1' && @['type'] == 'card']", - "required": ["credential"], - "properties": { - "credential": { - "anyOf": [ - { - "properties": {"type": {"const": "pan"}}, - "required": ["cvc"] - }, - { - "properties": {"type": {"const": "network_token"}}, - "required": ["cryptogram", "eci_value"] - } - ] - } - } - } +"requires": { + "protocol": { "min": "2026-01-23", "max": "2026-09-01" }, + "capabilities": { + "dev.ucp.shopping.checkout": { "min": "2026-06-01" } } } ``` -One path selects the submitted instrument, and one Object Constraint describes -it. The sibling `required` applies to every matching instrument; the `anyOf` -branches then apply to the nested `credential` object, which must satisfy at -least one. Each branch pins `type` with `const`, so a branch matches only the -credential family it describes; -[`payment_credential.json`](site:schemas/common/types/payment_credential.json) -already requires `type` on every credential, so no branch has to name it in -`required`. A [PAN credential](site:schemas/common/types/pan_credential.json) -without a `cvc` fails, as does a [network -token](site:schemas/common/types/network_token_credential.json) missing its -`eci_value`. - -Because every branch pins the discriminator, the branch set also closes the -accepted credential families. A handler -[token credential](site:schemas/common/types/token_credential.json) is a valid -credential at this position but satisfies neither branch, so this Business does -not accept it at this path. A Business that later accepts another family adds a -branch for it. - -Two separately targeted constraints cannot express this rule. Request -Constraints conjoin, so one value requiring `cvc` and another requiring -`cryptogram` would require both. Discriminating through the path filter instead -— selecting `pan` credentials in one value and `network_token` credentials in -another — moves conditional logic into the selector, which paths do not carry. - -## Actions - -An Action is an outstanding unit of extension-defined work for a Platform to -process. Its presence means the effect defined by its Action type is gated. -Actions appear only in responses, under the `actions` map. The common -fields identify the work but do not define how to process it; the active -extension does. - -This section defines the common Actions shape and the invariants every adopting -response shares. The shape is reusable, but a capability supports Actions only -when its specification explicitly adopts it and defines the parent-specific -behavior: where Actions appear, the effect each Action type gates, how Messages -apply, and how a later response reflects processing. Schema composition alone -does not establish support. Cart, Checkout, and Catalog adopt this shape; see -[Cart — Actions](../shopping/cart/index.md#actions), -[Checkout — Actions](../shopping/checkout/index.md#actions), and -[Catalog — Actions](../shopping/catalog/index.md#actions) for their parent-specific -contracts. - -Actions and Messages have different roles. An Action represents outstanding -work: it carries an identity and extension-owned processing configuration. A -Message communicates explanatory or diagnostic context about the current -response and can identify an exact Action occurrence through its RFC 9535 -`path`. When a Message includes `path`, the Business **MUST** make it an RFC -9535 JSONPath expression relative to the root of the containing UCP response -object. -Messages do not define how an Action is processed or determine its outcome, and -neither an Action nor a Message requires the other. - -For example, a Business can surface one outstanding Action beside an -explanatory Message (an illustrative, partial fragment): - - -```json -{ - "actions": { - "com.example.identity.student_verification": [ - { - "id": "verify-student-1", - "config": { - "verification_url": "https://business.example.com/verify/abc" - } - } - ] - }, - "messages": [ - { - "type": "info", - "code": "eligibility_accepted", - "content": "Student discount applied provisionally. Verify your status.", - "path": "$.actions['com.example.identity.student_verification'][0]" - } - ] -} -``` - -The Action identifies the outstanding work and carries extension-owned -processing configuration under `config`. The Message's `path` selects the exact -Action occurrence it explains. The -[checkout eligibility example](../shopping/checkout/index.md#eligibility-verification-at-completion) -composes this pattern into a complete Student Verification flow. - -For a newly processed successful response from a capability that adopts Actions, -the Business **MUST** include every outstanding Action and **MUST** omit -`actions` when none are outstanding. - -Cart and Checkout define request idempotency separately. Duplicate requests -follow those existing rules and can return the original cached response, -including its `actions` (see -[Message Signatures — Replay Protection](../signatures.md#replay-protection)). - -An Action's gate and an operation-specific outcome are orthogonal. Neither a -parent status nor a Message's type or severity determines whether an Action -gates its Action-defined effect. A Message explains the response or reports the -outcome of a particular requested effect. A Business **MAY** include an info or -warning Message whose `path` selects an outstanding Action to explain the -current response without reporting an operation failure. For a state-changing -operation whose requested effect was not applied because of an Action, the -Business **MUST** instead return the current resource with a `recoverable` error -Message whose `path` selects the exact Action occurrence. - -The Business's response is authoritative for the state after an operation: the -returned resource, together with any parent lifecycle its capability defines, is -the source of truth. The Action-type contract defines how the Business observes -processing, and the Platform then follows the containing capability's operation -contract. - -When an Action prevents a Cart or Checkout operation from succeeding, -processing the Action does not repeat that operation. If the Platform wants to -try again, it submits a new operation under the existing -[Replay Protection](../signatures.md#replay-protection) rules. +Keys in `requires.capabilities` **MUST** be a subset of the +extension's `$defs` keys. These ranges verify dependencies after exact +versions are selected; they do not select versions. The `ucp.version` is +fixed first by [profile selection](#protocol-version) and capability versions +by the [intersection algorithm](#intersection-algorithm); then, if `requires` +is present, Platforms and Businesses **MUST** verify that the selected +`ucp.version` and capability versions satisfy the declared constraints during +schema resolution. Incompatible extensions are excluded from the active +capability set (see [Resolution Flow](#resolution-flow)). If `requires` is +absent, the extension is assumed to be compatible with the versions declared +by the profile. -Each Action key is a reverse-domain **Action type**: the name identifies the -type of outstanding work, which is not necessarily the name of the extension -that declares it. An active extension declares each Action type and defines its -`config`, how a Platform processes it, its trust and fallback, and its outcomes. -A single extension can declare more than one Action type. Each declaring -extension contributes its Action-type keys to the containing capability's -schema through `allOf` composition (see -[Schema Composition](#schema-composition)), and capability negotiation selects -which extensions are active. Negotiating an extension activates the whole -contract it declares, including every Action type within it. +#### Schema Resolution Convention -Action type keys follow existing [Namespace Governance](#namespace-governance) -rules: an extension can declare only types within a reverse-domain namespace -controlled by its schema authority. An extension can use its own name as the key -for a single Action type — as the -[Student Verification example](../shopping/checkout/index.md#eligibility-verification-at-completion) -does — or declare several Action types under distinct keys. Each value is a -non-empty array of outstanding instances of that one Action type. The key -identifies the type, so an instance carries no separate type discriminator; a -Business surfaces multiple outstanding instances of the same type as multiple -entries in that array. +To validate payloads, implementations resolve extension schemas as follows: -The `actions` map does not define a processing order across Action types. Within -a single type's array, JSON preserves the order of its instances, and the -extension that declares the type defines whether that order carries processing -meaning. When ordering across Action types matters, the declaring extension -defines the sequencing and which Action types become outstanding at each step. +1. Determine the root capability from the operation (e.g., checkout operations + use `dev.ucp.shopping.checkout`) +2. For each active extension, resolve and apply its `$defs[{root_capability}]` -For example (illustrative only), a negotiated vendor extension -`com.example.payment.authentication` declares two Action types: -`com.example.payment.authentication.device_data_collection`, an invisible -device- and browser-data collection step, and -`com.example.payment.authentication.three_ds_challenge`, a Buyer-facing -authentication step. Because the collection step precedes the challenge, the -Business can emit the `device_data_collection` type first and, once its instance -is processed, emit the `three_ds_challenge` type in a later response. This shows -one extension declaring multiple Action types and sequencing them across -responses; it does not standardize device data collection or the authentication -challenge, which are illustrative here. +**Example:** A checkout response includes the discount extension. -Every instance shares a set of common fields: +- Root capability: `dev.ucp.shopping.checkout` +- Extension schema: `discount.json` +- Resolve: `discount.json#/$defs/dev.ucp.shopping.checkout` -- `id` — a non-empty identifier for the Action instance. -- `config` — an optional extension-owned configuration object. +#### Resolution Flow -`id` is required on every instance; `config` is optional. An extension defines -the instance-specific data a Platform needs to process its work under `config`; -`config` is the extension-owned channel for that data. +Platforms **MUST** resolve schemas following this sequence: -An Action instance also remains open to additional top-level fields for forward -compatibility. A Platform **MUST** tolerate and ignore Action instance -fields it does not recognize. +1. **Discovery**: Fetch business profile from `/.well-known/ucp` +2. **Negotiation**: Compute capability intersection (see + [Intersection Algorithm](#intersection-algorithm)) +3. **Schema Fetch**: Fetch base schema and all active extension schemas +4. **Version Compatibility**: For each fetched extension schema, + if `requires` is present, verify the negotiated protocol version + and capability versions satisfy the declared constraints. Exclude + incompatible extensions and re-prune orphaned extensions + (steps 3-4 of the [Intersection Algorithm](#intersection-algorithm)) +5. **Compose**: Merge schemas via `allOf` chains based on active extensions +6. **Validate**: Validate requests and responses against the composed schema -The Business **MUST** use a distinct `id` for each Action instance in a response. +### Profile Structure -When successive responses represent the same parent resource, the Business -**MUST** keep the same Action type key and `id` while the same work remains -outstanding. Replacement work **MUST** have a new `id`, and the Business -**MUST NOT** reuse an `id` during that resource's lifetime. +Profile documents are machine-readable discovery documents. Businesses publish +their profile at `/.well-known/ucp`; platforms publish their profile at the URI +advertised in `UCP-Agent`. -Otherwise, the common Actions contract defines no identity relationship between -Actions in independent responses. Equal `id` values alone do not identify the -same work. +A profile document is a JSON object with a required `ucp` member. The `ucp` +member contains protocol metadata: protocol version, services, optional +capabilities, and payment handlers. -A Business **MUST** emit an Action type only when an extension that declares it -is active for the containing capability in the negotiated intersection. The -composed JSON Schema can validate the common fields and each declared type's key -and `config` shape, but confirming that the declaring extension is active also -requires the negotiated capability context. +For both business and platform profiles, `ucp.version`, `ucp.services`, and +`ucp.payment_handlers` are required. The `services` and `payment_handlers` +registries **MUST** be present even when empty. `ucp.capabilities` is optional +and **MAY** be omitted, though useful commerce profiles normally advertise at +least one capability. -### Trust and Execution Boundaries +Profiles **MAY** include public JSON Web Keys used for HTTP Message +Signatures and signed webhooks. When a profile publishes signing keys, +they **MUST** appear in the top-level `keys[]` array — the canonical +UCP profile field that every UCP verifier reads. `keys[]` is a JWK Set +per [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517), so the +same document is simultaneously a UCP profile and a valid JWK Set — +which a signer can reuse as its Web Bot Auth key source. See +[Deployment Patterns for WBA Interop](#deployment-patterns-for-wba-interop) +below. -Negotiating an extension confirms support for its complete Action-type contract -before runtime. That agreement does not make every future runtime value or -delegate trusted. Each instance remains subject to the composed schema, the -Action-type contract, and Platform policy. +Adding, rotating, or removing a key updates this single array. Removal +is the security-critical case: a revoked or compromised key is not +effectively revoked until it is absent from `keys[]`. -The active Action-type contract defines which `config` fields a Platform processes -and what they mean. A Platform **MUST NOT** treat any other field as an -instruction to load content, render HTML, execute code, run a shell command, or -invoke a native API. +UCP defines two well-known key types: **EC** (ECDSA P-256, P-384) and +**OKP** (EdDSA Ed25519); the key-type, curve, and algorithm +vocabularies are open and verifiers skip keys they do not recognize. +See [Message Signatures](../signatures.md) for key format, algorithms, +lookup, and rotation. -A Platform **MAY** apply additional trust or runtime policy and **MAY** decline -any instance that does not satisfy it. Supporting a whole extension does not -require a Platform to accept every runtime value. +#### Business Profile -A Platform **MUST NOT** assume that the effect gated by an Action succeeded -merely because an Action surface or external interaction completed. A later -response from the Business, together with any parent lifecycle its capability -defines, remains authoritative for that outcome. +Businesses publish their profile at `/.well-known/ucp`. An example: -The declaring extension defines the concrete trust, execution, and fallback rules. -The common Actions contract defines no generic machinery: no URL scheme, origin, -or delegate policy; no sandbox, permission, or presentation model; no timeout, -failure, or recovery model; and no callback, result, state, polling, or -executor. Each concrete Action type adds only the machinery its own processing -requires. - -## Discovery, Governance, and Negotiation - -UCP separates [protocol version selection](#protocol-version) from -[capability negotiation](#capability-versions). A Business advertises its current -protocol version and links to profiles for older supported versions. After the -Platform selects one exact version, the Business determines the active -capabilities from the versions both parties advertise. Version lifecycle, -including when to remove an older version, is a Business policy decision; UCP -does not prescribe a deprecation schedule. Business and Platform profiles can be -cached by both parties. - -### Namespace Governance - -UCP uses reverse-domain naming to encode governance authority directly into -capability identifiers. This eliminates the need for a central registry. - -#### Naming Convention - -All capability and service names **MUST** use the format: - -```text -{reverse-domain}.{service}.{capability} + +```json +{ + "ucp": { + "version": "{{ ucp_version }}", + "services": { + "dev.ucp.shopping": [ + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview/", + "transport": "rest", + "endpoint": "https://business.example.com/ucp/v1", + "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/rest.openapi.json" + }, + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview/", + "transport": "mcp", + "endpoint": "https://business.example.com/ucp/mcp", + "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/mcp.openrpc.json" + }, + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview/", + "transport": "a2a", + "endpoint": "https://business.example.com/.well-known/agent-card.json" + }, + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview/", + "transport": "embedded", + "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/embedded.openrpc.json" + } + ] + }, + "capabilities": { + "dev.ucp.shopping.checkout": [ + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/shopping/checkout", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/checkout.json" + } + ], + "dev.ucp.shopping.fulfillment": [ + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/shopping/extensions/fulfillment", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/fulfillment.json", + "extends": "dev.ucp.shopping.checkout" + } + ], + "dev.ucp.shopping.discount": [ + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/shopping/extensions/discount", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/discount.json", + "extends": "dev.ucp.shopping.checkout" + } + ], + "dev.ucp.common.identity_linking": [ + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/common/identity-linking/", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/common/identity_linking.json", + "config": { + "providers": { + "com.example.idp": [ + { "type": "oauth2", "auth_url": "https://accounts.example.com/" } + ] + }, + "scopes": { + "dev.ucp.shopping.order:read": {}, + "dev.ucp.shopping.order:manage": {} + } + } + } + ] + }, + "payment_handlers": { + "com.example.processor_tokenizer": [ + { + "id": "processor_tokenizer", + "version": "{{ ucp_version }}", + "spec": "https://example.com/specs/payments/processor_tokenizer", + "schema": "https://example.com/specs/payments/merchant_tokenizer.json", + "available_instruments": [ + { + "type": "card", + "constraints": { + "properties": { "brand": { "enum": ["visa", "mastercard", "amex"] } } + } + } + ], + "config": { + "type": "CARD", + "tokenization_specification": { + "type": "PUSH", + "parameters": { + "token_retrieval_url": "https://api.psp.example.com/v1/tokens" + } + } + } + } + ] + } + }, + "keys": [ + { + "kid": "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U", + "kty": "OKP", + "crv": "Ed25519", + "x": "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs", + "use": "sig", + "alg": "EdDSA" + }, + { + "kid": "business_2025", + "kty": "EC", + "crv": "P-256", + "x": "qIVYZVLCrPZHGHjP17CTW0_-D9Lfw0EkjqF7xB4FivA", + "y": "Mc4nN9LTDOBhfoUeg8Ye9WedFRhnZXZJA12Qp0zZ6F0", + "use": "sig", + "alg": "ES256" + } + ] +} ``` -**Components:** - -- `{reverse-domain}` - Authority identifier derived from domain ownership -- `{service}` - Service/vertical category (e.g., `shopping`, `common`) -- `{capability}` - The specific capability name - -**Examples:** - -| Name | Authority | Service | Capability | -| ----------------------------------- | ----------- | -------- | ---------------- | -| `dev.ucp.shopping.checkout` | ucp.dev | shopping | checkout | -| `dev.ucp.shopping.fulfillment` | ucp.dev | shopping | fulfillment | -| `dev.ucp.common.identity_linking` | ucp.dev | common | identity_linking | -| `com.example.payments.installments` | example.com | payments | installments | - -#### Authority Binding - -Reverse-domain names serve two purposes: collision-safe **identifiers** (keys -and references), and **entities** — capabilities, services, and payment -handlers — that declare a fetched `schema` URL describing them. Authority -binding applies to every entity with a remote `schema`: a declared `schema` -URL's origin **MUST** match the namespace authority in its name. - -A capability **MUST** declare a `schema`; services and payment handlers declare -one where their transport or handler defines it. Each entity **MAY** also -declare a `spec` URL (human-readable documentation). - -This binding guarantees **provenance, not trust**: a valid binding proves only -that the reverse-domain name is controlled by the party that owns the -corresponding domain — an entity cannot be published under a namespace its -author does not control. It does **not** assert that the entity is trustworthy, -correct, or worth supporting. Whether to negotiate, trust, or implement it is -always the client's decision; this binding only tells the client *who* is making -the claim. Provenance is established from domain ownership and evaluated at -negotiation time. - -The `spec` URL is documentation, not part of the machine trust path, so its -origin is **not** authority-bound: it **MUST** be `https` but **MAY** be served -from any host (e.g. a docs subdomain or third-party docs host). Only the -`schema` URL carries the authority binding defined below. +The business profile advertises the business's available transports, +capabilities, payment handlers, and public verification keys. This +example publishes signing keys in the canonical top-level `keys[]` +array (an RFC 7517 JWK Set), so the same document is also a valid JWK +Set — reusable as a Web Bot Auth key source. Every UCP verifier reads +`keys[]`, whether it resolved the key via `UCP-Agent` or via +`Signature-Agent`. -##### Derivation algorithm +A WBA-shape verifier reads `keys[]` from this profile **only when the +`Signature-Agent` header selects it** with `type=jwks_uri` (or `type=cimd`) +pointing at the profile URL. The default `type=directory` (when `type` is +omitted) instead expects a *signed* directory document at +`/.well-known/http-message-signatures-directory`, not a static profile, so +it will not read `keys[]` from a static `/.well-known/ucp`. See +[Deployment Patterns for WBA Interop](#deployment-patterns-for-wba-interop). -The authority is derived **from the `schema` URL host** — which names the -owning domain directly, with no ambiguity about where the domain ends — and -validated as a label-aligned prefix of, or an exact match for, the entity's -name. For the `schema` URL of an entity whose name is `name`, a platform -**MUST** apply the following: +This example uses two keys. Whether a deployment needs one or two depends +on the algorithms its counterparties accept — many need only one; see +[Signature Algorithms](../signatures.md#signature-algorithms). The two keys +here: -1. Parse the URL with a conformant (WHATWG) URL parser. It **MUST** parse, - **MUST** use the `https` scheme, and **MUST NOT** contain userinfo (a - `user:pass@` component). Substring matching on the raw URL is **NOT** - permitted — e.g. `https://ucp.dev@evil.example/x.json` has host - `evil.example`, not `ucp.dev`. -2. The host **MUST** be a registered domain name of at least two labels. - IP-literal hosts (`https://203.0.113.10/...`) and single-label hosts - (`https://localhost/...`) are invalid authorities. -3. Take the URL's hostname (the host without any port), normalize it (lowercase; - strip a trailing `.`; internationalized domains in A-label / punycode form), - and **reverse its labels** to form the `authority_prefix` (host `ucp.dev` → - `dev.ucp`). -4. The binding is valid if and only if **either** of the following holds: - - **Exact match** — `name` equals `authority_prefix`. The name is itself the - reversed host, so the publisher demonstrably controls the entire namespace. - This is the shape for an entity whose identity is a bare controlled domain, - such as a payment handler `com.example.pay` served from `pay.example.com` - (reversed host `com.example.pay` equals the name). - - **Prefixed** — `name` is `authority_prefix`, then a `.`, then one or more - further labels; that is, the character immediately after `authority_prefix` - in `name` is a `.`. Requiring that separating `.` keeps the match on a - label boundary — it stops `com.example` (host `example.com`) from matching - a neighboring namespace like `com.examplecorp.*`, where `com.example` is a - textual prefix but not a label-aligned one. +- An **Ed25519** key (OKP) for HTTP transport identity, WBA-compatible. + The `kid` is the JWK SHA-256 Thumbprint per RFC 7638. +- An **ECDSA P-256** key (EC) for AP2 mandate signing + (`ap2.merchant_authorization`). -Authority binding establishes **provenance only** — that the name is controlled -by the party serving its `schema`. It does **not** require any label beyond the -authority itself. The `{reverse-domain}.{service}.{capability}` shape is a -separate [Naming Convention](#naming-convention) that governs capability and -service names — it does not apply to payment handlers — and is validated -independently of this check. +A business that does not interact with AP2 or WBA may publish a single +ES256 key in `keys[]` (the universal baseline). See +[Key Discovery](#key-discovery) for key lookup and resolution, +[Deployment Patterns for WBA Interop](#deployment-patterns-for-wba-interop) +for hosting choices, and [Message Signatures](../signatures.md) for +signing mechanics. -Any labels after the authority prefix are treated as opaque by this check; they -are not inspected or split. +Businesses that support older protocol versions **SHOULD** include a +`supported_versions` object mapping each older version to a +version-specific profile URI. See [Protocol Version](#protocol-version) +for details. -| Entity name | `schema` host | `authority_prefix` | Result | -| ----------------------------------- | ------------------ | ------------------ | ------------------- | -| `dev.ucp.shopping.checkout` | `ucp.dev` | `dev.ucp` | **accept** (prefix) | -| `dev.ucp.shopping.checkout` | `shopping.ucp.dev` | `dev.ucp.shopping` | **accept** (prefix) | -| `com.example.payments.installments` | `example.com` | `com.example` | **accept** (prefix) | -| `com.example.pay` | `pay.example.com` | `com.example.pay` | **accept** (exact) | -| `com.example.pay` | `example.com` | `com.example` | **accept** (prefix) | -| `com.example.pay` | `evil.example` | `example.evil` | **reject** | -| `dev.ucp.shopping.checkout` | `evil.example` | `example.evil` | **reject** | -| `com.examplecorp.pay` | `example.com` | `com.example` | **reject** | -| `com.example.pay` | `cdn.example.com` | `com.example.cdn` | **reject** | +#### Platform Profile -An entity's `schema` is served from a host whose reversed labels either **equal** -its name or are a **label-aligned prefix** of it. A host whose reversed labels -are exactly the name (`pay.example.com` for `com.example.pay`) satisfies the -exact case; a canonical apex host (`example.com` for `com.example.*`) satisfies -the prefix case; a subdomain satisfies the prefix case only when its labels line -up with the namespace path (`shopping.ucp.dev` for `dev.ucp.shopping.*`). Because -a parent domain's reversed labels are also a prefix, a name such as -`com.example.pay` binds equally from its exact host (`pay.example.com`) or a -parent authority (`example.com`) — both prove control. Unrelated subdomains such -as a shared CDN do **not** satisfy any case — host the canonical schema on a -name-aligned origin. +Platform profiles are similar and include signing keys for capabilities +requiring cryptographic verification. Capabilities **MAY** include a `config` +object for capability-specific settings (e.g., callback URLs, feature flags). An +example: -The check uses the `schema` URL host directly and does not consult the -[Public Suffix List](https://publicsuffix.org/), so it treats a **public -suffix** — a domain under which independent parties can register names, from -`co.uk` to the list's private-section suffixes operated by services that let -third parties register subdomains or buckets (`github.io`, object storage, app -platforms) — as an ordinary authority. Co-tenants under such a suffix satisfy -the same prefix, so declare entities only under a **registrable domain** (a -public suffix plus one label) that you exclusively control. + +```json +{ + "ucp": { + "version": "{{ ucp_version }}", + "services": { + "dev.ucp.shopping": [ + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview/", + "transport": "rest", + "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/rest.openapi.json", + "endpoint": "https://platform.example.com/ucp/v1" + } + ] + }, + "capabilities": { + "dev.ucp.shopping.checkout": [ + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/shopping/checkout", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/checkout.json" + } + ], + "dev.ucp.shopping.fulfillment": [ + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/shopping/extensions/fulfillment", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/fulfillment.json", + "extends": "dev.ucp.shopping.checkout" + } + ], + "dev.ucp.shopping.order": [ + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/shopping/order", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/order.json", + "config": { + "webhook_url": "https://platform.example.com/webhooks/ucp/orders" + } + } + ], + "dev.ucp.common.identity_linking": [ + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/common/identity-linking/", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/common/identity_linking.json" + } + ] + }, + "payment_handlers": { + "com.google.pay": [ + { + "id": "gpay_1234", + "version": "2024-12-03", + "spec": "https://developers.google.com/merchant/ucp/guides/gpay-payment-handler", + "schema": "https://pay.google.com/gp/p/ucp/2026-01-11/schemas/gpay_config.json" + } + ], + "dev.shopify.shop_pay": [ + { + "id": "shop_pay_1234", + "version": "{{ ucp_version }}", + "spec": "https://shopify.dev/ucp/shop-pay-handler", + "schema": "https://shopify.dev/ucp/schemas/shop-pay-config.json", + "available_instruments": [ + {"type": "shop_pay"} + ] + } + ], + "com.example.processor_tokenizer": [ + { + "id": "processor_tokenizer", + "version": "{{ ucp_version }}", + "spec": "https://example.com/specs/payments/processor_tokenizer-payment", + "schema": "https://example.com/schemas/payments/delegate-payment.json", + "available_instruments": [ + {"type": "card", "constraints": {"properties": {"brand": {"enum": ["visa", "mastercard"]}}}} + ] + } + ] + } + }, + "keys": [ + { + "kid": "platform_2025", + "kty": "EC", + "crv": "P-256", + "x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", + "y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", + "use": "sig", + "alg": "ES256" + } + ] +} +``` -##### Enforcement +### The `ucp` Protocol Namespace -A platform **MUST** validate each business-declared `schema` URL before fetching -it. If the URL's origin does not match the entity's namespace authority (per -[Derivation algorithm](#derivation-algorithm)), the platform **MUST NOT** fetch -it and **MUST** reject the entity — treated as not present and never -activated. A `spec` URL **MUST** be a valid `https` URL. A platform **MUST NOT** follow redirects (`3xx`) when fetching a `schema` URL, consistent with profile fetches. +The member name `ucp` is reserved as the **protocol namespace** in every +structured UCP object scope — an object whose members are schema-defined +fields. The top-level `ucp` member that profiles and responses carry — +described in [Profile Structure](#profile-structure) above — is not a special +wrapper; it is the root manifestation of this reservation: a reserved member +of the root object. The reservation does not apply to a dictionary container, +whose keys are data rather than fields. A dictionary key named `ucp` is +ordinary data. A structured object used as a dictionary value remains an +eligible scope. Schema authors **MUST NOT** define a domain field named `ucp` +in structured object schemas or extensions. -The platform fetches and composes business-declared schemas to validate every -request and response, so validating the binding ensures each composed schema is -sourced from the party that owns the entity's namespace. A business **SHOULD** -apply the same check to the platform profile and exclude any entity whose -binding fails. +At each eligible structured scope, `ucp` carries the protocol's statements +about that scope: protocol metadata at the root (version, services, +capabilities, payment handlers) and structural annotations such as +[`map_order`](#map_order). -Binding validates the declared hostname for provenance; it is **not** a -fetch-safety control and does not authorize dereferencing. Fetching the `schema` -URL — like any URL fetched during discovery — is additionally subject to the -protocol's URL fetch-safety requirements, which guard the *resolved* address -(not just the hostname) against server-side request forgery toward special-use -or cloud-metadata addresses and DNS rebinding. The hostname check and the -resolved-address check are independent, and both apply. +**Openness.** The `ucp` container is open. Consumers **MUST** ignore members +inside `ucp` that they do not recognize (tolerant reader). Openness exists so +documents produced under a newer UCP version remain readable by older +consumers — it is *not* extension space. Only UCP core defines members inside +`ucp`, and extension authors **MUST NOT** place extension data there. An +unrecognized member inside `ucp` means "defined by a newer UCP version," +never "extension data." -#### Governance Model +**No direct recursion.** Producers **MUST NOT** emit a `ucp` member as a direct +child of another `ucp` member (`ucp.ucp`). If one is present, a receiving +Business or Platform **MUST NOT** interpret it as another protocol namespace and +**MUST** ignore that child. Structured objects beneath the namespace, such as a +capability's `config`, remain eligible for their own `ucp` member. -| Namespace Pattern | Authority | Governance | -| ----------------- | ------------ | ------------------- | -| `dev.ucp.*` | ucp.dev | UCP governing body | -| `com.{vendor}.*` | {vendor}.com | Vendor organization | -| `org.{org}.*` | {org}.org | Organization | +**Ambient vocabulary.** The protocol namespace is ambient within structured +UCP objects: a Business or Platform **MAY** include a `ucp` member at any +eligible structured scope, and its contents are defined exclusively by UCP +core's vocabulary — the member is part of the UCP document grammar, like the +name reservation itself. The reservation stops at a dictionary container. A +Business or Platform **MUST NOT** interpret a dictionary key named `ucp` as +the protocol namespace; the key and its value are ordinary dictionary data. +For example, `attribution` is a dictionary of string values, so an attribution +key named `ucp` is ordinary attribution data, not a protocol-namespace member. +Guidance for schema authors on working within this reservation lives in the +Schema Authoring Guide's +[The Reserved `ucp` Member](/documentation/schema-authoring/#the-reserved-ucp-member) +section. A Business or Platform encountering a `ucp` member at an eligible +structured scope processes the members it recognizes, each per its own +definition, and **MUST** ignore unrecognized members (see *Openness* above). +A member is admitted to the vocabulary only if it is safe to ignore: a +Business or Platform that does not process it loses only that member's +benefit, never correctness. A Business or Platform that ignores `map_order`, +for example, simply traverses the map unordered — the status quo before +ordering existed. -The `dev.ucp.*` namespace is reserved for capabilities sanctioned by the UCP -governing body. Vendors **MUST** use their own reverse-domain namespace for -custom capabilities. +**Scope determines obligations.** At the root of profiles and responses, the +`ucp` envelope additionally carries the required protocol metadata exactly as +specified elsewhere in this document — this section changes none of those +obligations. A Business or Platform **MAY** omit the member at every other +eligible structured scope. Dictionary containers are not eligible scopes and +carry no protocol-namespace obligation. Conformance to the vocabulary is +defined by this specification's processing rules, not by ordinary instance +validation against open UCP source schemas; that validation treats ambient +`ucp` members as ignored unknown objects. -### Services +**Vocabulary applicability.** Each registered protocol-namespace member defines +the document contexts and message directions where it applies. -A **service** defines the API surface for a vertical (shopping, common, etc.). -Services include operations, events, and transport bindings defined via -standard formats: +**Schema processing.** UCP source schemas are open by default, so ordinary +validation against them may accept ambient `ucp` without applying the protocol +vocabulary. For a selected message direction, a UCP-aware resolver **MUST** +produce a resolved schema that recognizes and validates ambient `ucp` at every +eligible structured scope against the central vocabulary in +`ucp.json#/$defs/members`, subject to that vocabulary's applicability in the +selected direction. The `ucp` namespace remains open to unrecognized members +for forward compatibility, even if the resolved schema rejects other unknown +domain fields. The result is ordinary JSON Schema that standard validators and +code generators can consume. -- **REST**: OpenAPI 3.x (JSON format) -- **MCP**: OpenRPC (JSON format) -- **A2A**: Agent Card Specification -- **EP(embedded)**: OpenRPC (JSON format) +#### `map_order` -A service is identified by its reverse-domain registry key (e.g., -`dev.ucp.shopping`). In a profile, services are keyed by that name, and each -entry in `services[name][]` pairs the service with one transport binding and -declares the service `version`: in release `D` that version is `D`. This is the -service version, not a transport version — the binding has no separate version. -The OpenAPI or OpenRPC artifact a binding references carries its own -`info.version` as release metadata, not a separate version to negotiate. See -[Component Versioning and Release Snapshots](#component-versioning-and-release-snapshots). +JSON object members are unordered: member order is not guaranteed to survive +parsing, and +[RFC 8785](https://www.rfc-editor.org/rfc/rfc8785.html){ target="_blank" } +(JSON Canonicalization Scheme), which UCP signing relies on, sorts object +member names while preserving array element order. `map_order` uses an array +so its declared order survives canonicalization and signing. -#### Service Definition +`map_order` declares a preferred key-traversal order for map-valued fields in +the scope annotated by its containing `ucp` member. At a nested scope, each +key of `map_order` names a map field on the object that contains `ucp`. At the +document root, each key instead names a sibling map field inside the root `ucp` +envelope. Root domain fields outside `ucp`, such as a checkout response's +`actions`, are not targets. Each value is an array of the target map's keys in +preferred traversal order. -{{ extension_schema_fields('service.json#/$defs/platform_schema', 'overview') }} +`map_order` does not apply to UCP operation requests. -Transport definitions **MUST** be thin: they declare method names and reference -base schemas only. See [Requirements](#requirements) for details. +For a target map field `` and its companion array `map_order.`: -#### Endpoint Resolution +1. Producers **MUST NOT** rely on JSON object member order for UCP map-valued + registries; `map_order` is the order carrier. +2. `map_order.` contains keys from the target map field `` in + preferred traversal order. +3. The order array **MAY** be partial: listed keys are traversed first, in + array order. +4. Unlisted map keys remain valid and available; consumers traverse them + after the listed keys, using the field-defined fallback order or, if the + field defines none, the + [property-name ordering defined by RFC 8785](https://www.rfc-editor.org/rfc/rfc8785.html#section-3.2.3){ target="_blank" }. +5. The order array is not an allowlist: consumers **MUST NOT** interpret + omission of a key as removal, ineligibility, or reduced support. +6. Producers **MUST** name a present, map-valued target and **MUST** list only + keys present in that target map. A receiving Business + or Platform **MUST NOT** reject the containing document solely because an + entry names an absent, unrecognized, or non-map target, or a key absent from + its target map. If it processes `map_order`, it **MUST** ignore the unusable + entry. +7. Producers **MUST NOT** list the same map key more than once in an order + array. Consumers **MUST NOT** reject the containing document solely because + an order array repeats a map key. Consumers that process `map_order` + **MUST** honor the first occurrence and ignore later repetitions. +8. If `map_order`, or its entry for a field, is absent, no order is declared; + consumers **MUST NOT** fall back to object member order. +9. A field's own specification defines what ordered traversal *means* for it + (presentation, negotiation priority, and so on) — `map_order` carries + order and nothing else. -The `endpoint` field provides the base URL for API calls. OpenAPI paths are -appended to this endpoint to form the complete URL. +Rules 3–5 are a deliberate divergence from conventions in which unlisted keys +are an error or are dropped: partial lists are always valid, and unlisted +keys are always retained. -**Example:** +A business profile ordering its payment handlers: - + ```json { - "version": "{{ ucp_version }}", - "transport": "rest", - "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/rest.openapi.json", - "endpoint": "https://business.example.com/api/v2" + "ucp": { + "version": "{{ ucp_version }}", + "services": { ... }, + "payment_handlers": { + "com.google.pay": [ + { "id": "gpay", "version": "{{ ucp_version }}" } + ], + "dev.shopify.shop_pay": [ + { "id": "shop_pay", "version": "{{ ucp_version }}" } + ] + }, + "map_order": { + "payment_handlers": ["dev.shopify.shop_pay", "com.google.pay"] + } + } } ``` -With OpenAPI path `/checkout-sessions`, the resolved URL is: +`map_order` is scope-generic — the same mechanism orders sibling maps at any +eligible structured scope, as when a Business orders the identity-provider +registry inside a capability's `config`: -```text -POST https://business.example.com/api/v2/checkout-sessions + +```json +{ + "providers": { + "app.example.login": [ + {"type": "oauth2", "auth_url": "https://login.example.app"} + ], + "com.google": [ + {"type": "oauth2", "auth_url": "https://accounts.google.com"} + ] + }, + "ucp": { + "map_order": {"providers": ["app.example.login", "com.google"]} + } +} ``` -**Rules:** - -- `endpoint` **MUST** be a valid URL with scheme (https) -- `endpoint` **SHOULD NOT** have a trailing slash -- OpenAPI paths are relative and appended directly to endpoint -- Same resolution applies to MCP endpoints for JSON-RPC calls -- `endpoint` for A2A transport refers to the Agent Card URL for the agent +What an order *means* remains per-field (rule 9); the one traversal semantics +defined today is the business's presentation preference for +`payment_handlers` — see [Payment Handlers](#payment-handlers). -### Capabilities +### Platform Advertisement on Request -A **capability** is a feature within a service. It declares what -functionality is supported and where to find documentation and schemas. +Platforms **MUST** communicate their profile URI with each request to enable +capability negotiation. -#### Capability Definition +**HTTP Transport:** Platforms **MUST** use Dictionary Structured Field syntax +([RFC 8941](https://datatracker.ietf.org/doc/html/rfc8941){ target="_blank" }) +in the UCP-Agent header: -{{ extension_schema_fields('capability.json#/$defs/platform_schema', 'overview') }} +```text +POST /checkout HTTP/1.1 +UCP-Agent: profile="https://agent.example/profiles/shopping-agent.json" +Content-Type: application/json -#### Extensions +{"line_items": [...]} +``` -An **extension** is an optional module that augments another capability. -Extensions use the `extends` field to declare their parent(s): +**MCP Transport:** Platforms **MUST** include a `meta` object containing request +metadata: - + ```json { - "dev.ucp.shopping.fulfillment": [ - { - "version": "{{ ucp_version }}", - "spec": "https://ucp.dev/{{ ucp_version }}/specification/shopping/extensions/fulfillment", - "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/fulfillment.json", - "extends": "dev.ucp.shopping.checkout" + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "create_checkout", + "arguments": { + "meta": { + "ucp-agent": { + "profile": "https://agent.example/profiles/shopping-agent.json" + } + }, + "checkout": { + "line_items": [...] + } } - ] + }, + "id": 1 } ``` -##### Multi-Parent Extensions +### Negotiation Protocol -Extensions **MAY** extend multiple parent capabilities by using an array: +#### Platform Requirements - -```json -{ - "dev.ucp.shopping.discount": [ - { - "version": "{{ ucp_version }}", - "spec": "https://ucp.dev/{{ ucp_version }}/specification/shopping/extensions/discount", - "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/discount.json", - "extends": ["dev.ucp.shopping.checkout", "dev.ucp.shopping.cart"] - } - ] -} -``` +1. **Profile Advertisement**: Platforms **MUST** include their profile URI in + every request using the transport-appropriate mechanism. +2. **Discovery**: Platforms **MAY** fetch the business profile from + `/.well-known/ucp` before initiating requests. If fetched, platforms + **SHOULD** cache the profile according to HTTP cache-control directives. +3. **Namespace Validation**: Before fetching, platforms **MUST** validate that + each capability's `schema` URL origin matches its namespace authority (see + [Authority Binding](#authority-binding)) and **MUST** reject capabilities + that fail this binding. +4. **Schema Resolution**: Platforms **MUST** fetch and compose schemas for + negotiated capabilities before making requests. -When an extension declares multiple parents: +#### Business Requirements -- The extension **MAY** define different fields for each capability it extends - (e.g., `loyalty_earned` for checkout, `loyalty_preview` for cart) -- See [Intersection Algorithm](#intersection-algorithm) for negotiation rules +1. **Profile Resolution**: Upon receiving a request with a platform profile + URI, businesses **MUST** fetch and validate the platform profile unless + already cached. Because businesses negotiate by capability name and serve + their own schemas, they do not normally dereference platform-declared + `schema` URLs; they **SHOULD** nonetheless verify the namespace binding (see + [Authority Binding](#authority-binding)) as defense in depth. +2. **Capability Intersection**: Businesses **MUST** compute the intersection of + platform and business capabilities. +3. **Extension Validation**: Extensions without their parent capability in the + intersection **MUST** be excluded. +4. **Response Requirements**: Businesses **MUST** include the `ucp` field in + every response containing: + - `version`: The UCP version used to process the request + - `capabilities`: Array of active capabilities for this response + +#### Intersection Algorithm + +The capability intersection algorithm determines which capabilities are active +for a session: + +1. **Compute intersection**: For each business capability, include it in the + result if a platform capability with the same `name` exists. + +2. **Select version**: For each capability in the intersection, compute the + set of version strings present in **both** the business and platform + arrays. If the set is non-empty, select the **highest** version + (latest date). If the set is empty (no mutual version), **exclude** the + capability from the intersection. + +3. **Prune orphaned extensions**: Remove any capability where `extends` is + set but **none** of its parent capabilities are in the intersection. + - For single-parent extensions (`extends: "string"`): parent must be present + - For multi-parent extensions (`extends: ["a", "b"]`): at least one parent + must be present + +4. **Repeat pruning**: Continue step 3 until no more capabilities are removed + (handles transitive extension chains). + +The result is the set of capabilities both parties support at mutually +compatible versions, with extension dependencies satisfied. + +#### Error Handling -Extensions can be: +UCP negotiation can fail in two ways: -- **Official**: `dev.ucp.shopping.fulfillment` extends `dev.ucp.shopping.checkout` -- **Vendor**: `com.example.installments` extends `dev.ucp.shopping.checkout` +1. **Discovery failure**: The business cannot fetch or parse the platform's + profile. -### Schema Composition +2. **Negotiation failure**: The provided profile is valid but capability + intersection is empty or versions are incompatible. -Extensions can add new fields and modify shared structures (e.g., discounts -modify `totals`, fulfillment adds fulfillment to `totals.type`). +Discovery failures are transport errors — the required inputs could +not be retrieved or were malformed. Negotiation failures are business +outcomes — the handler executed on the provided inputs and reported +the result in the UCP response: -#### Requirements +- **Discovery or version failure** → transport error with optional `continue_url` +- **Capability negotiation failure** → UCP response with optional `continue_url` -- Transport definitions (OpenAPI/OpenRPC) **MUST** reference base schemas - only. They **MUST NOT** enumerate fields or define payload shapes inline. -- Extensions **MUST** be self-describing. Each extension schema **MUST** - declare the types it introduces and how it modifies base types using `allOf` - composition. -- Platforms **MUST** resolve schemas client-side by fetching and composing - base schemas with active extension schemas. +##### Error Codes -#### Extension Schema Pattern +**Negotiation Errors:** -Extension schemas define composed types using `allOf`. The `$defs` key **MUST** -use the full parent capability name (reverse-domain format) to enable -deterministic schema resolution: +| Code | Description | REST | MCP | +| --------------------------- | ---------------------------------------------------- | ---- | ------ | +| `invalid_profile_url` | Profile URL is malformed, missing, or unresolvable | 400 | -32001 | +| `profile_unreachable` | Resolved URL but fetch failed (timeout, non-2xx) | 424 | -32001 | +| `profile_malformed` | Fetched content is not valid JSON or violates schema | 422 | -32001 | +| `version_unsupported` | Platform's protocol version not supported | 422 | -32001 | +| `capabilities_incompatible` | No compatible capabilities in intersection | 200 | result | - -```json -{ - "$defs": { - "discounts_object": { ... }, - "dev.ucp.shopping.checkout": { - "title": "Checkout with Discount", - "allOf": [ - {"$ref": "checkout.json"}, - { - "type": "object", - "properties": { - "discounts": { - "$ref": "#/$defs/discounts_object" - } - } - } - ] - } - } -} -``` +**Signature Errors:** -**Requirements:** +| Code | Description | REST | MCP | +| ---------------------- | ------------------------------------------------------ | ---- | ------ | +| `signature_missing` | Required signature header/field not present | 401 | -32000 | +| `signature_invalid` | Signature verification failed | 401 | -32000 | +| `key_not_found` | Key ID not found in signer's published key set | 401 | -32000 | +| `digest_mismatch` | Body digest doesn't match `Content-Digest` header | 400 | -32600 | +| `algorithm_unsupported`| Signature algorithm not supported | 400 | -32600 | -- Extension schemas **MUST** have a `$defs` entry for each parent declared in - `extends` -- The `$defs` key **MUST** match the parent's full capability name exactly +See [Message Signatures](../signatures.md) for signature verification details. -This convention ensures: +**Protocol Errors:** -- **Self-documenting**: The schema declares exactly which parents it extends -- **Deterministic resolution**: The `extends` value maps directly to the `$defs` key -- **Verifiable**: Build-time checks can confirm each `extends` entry has a - matching `$defs` key +| HTTP | Description | MCP | +| ---- | ----------------------------------------------- | ---------- | +| 401 | Authentication required or credentials invalid | -32000 | +| 403 | Authenticated but insufficient permissions | -32000 | +| 409 | Idempotency key reused with different payload | -32000 | +| 429 | Too many requests | -32000 | +| 500 | Unexpected server error | -32603 | +| 503 | Server temporarily unable to handle requests | -32000 | -##### Version Requirements +For MCP over HTTP, the HTTP status code is the primary signal; the JSON-RPC +`error.code` provides a secondary signal. Both transports **SHOULD** include +`Retry-After` header (REST) or `error.data.retry_after` (MCP) for 429 and 503 +responses. -Extension authors **SHOULD** declare a `requires` object in the extension -schema (alongside its `name`, `title`, and `description`) stating the versions -the extension depends on. A third-party extension schema declares its own -author-controlled `version`, which advances independently of `ucp.version`; -a UCP-authored `dev.ucp.*` extension declares version `D` in release `D`. -`requires.protocol` constrains the selected `ucp.version`, and -`requires.capabilities` constrains the selected versions of the named -capabilities: +The Embedded Protocol uses the same JSON-RPC error codes for peer-to-peer +communication between host and embedded context. Server-specific scenarios +(rate limiting, idempotency) do not apply to the embedded transport. See +[Embedded Protocol — Response Handling](../embedded-protocol.md#response-handling) +for the full error handling specification. - -```json -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://acme.com/ucp/schemas/loyalty.json", - "name": "com.acme.shopping.loyalty", - "version": "2026-06-15", - "title": "Acme Loyalty Points", - "requires": { - "protocol": { "min": "2026-01-23" }, - "capabilities": { - "dev.ucp.shopping.checkout": { "min": "2026-06-01" } - } - }, - "$defs": { - "dev.ucp.shopping.checkout": { ... } - } -} -``` +##### The `continue_url` Field -The schema author — not the profile publisher — declares version -requirements. The profile publisher selects and advertises compatible -versions in their profile. +When UCP negotiation fails, `continue_url` provides a fallback web experience. +Businesses **SHOULD** provide the most contextually relevant URL: -Each constraint is an object with a required `min` (inclusive) and -optional `max` (inclusive) version. When `max` is absent, there is -no upper bound: +- For checkout operations: link to the cart or checkout page +- For catalog operations: link to the product or search results +- As a fallback: link to the storefront homepage - -```json -"requires": { - "protocol": { "min": "2026-01-23", "max": "2026-09-01" }, - "capabilities": { - "dev.ucp.shopping.checkout": { "min": "2026-06-01" } - } -} -``` +This enables graceful degradation—agents can redirect buyers to complete their +task through the standard web interface. -Keys in `requires.capabilities` **MUST** be a subset of the -extension's `$defs` keys. These ranges verify dependencies after exact -versions are selected; they do not select versions. The `ucp.version` is -fixed first by [profile selection](#protocol-version) and capability versions -by the [intersection algorithm](#intersection-algorithm); then, if `requires` -is present, Platforms and Businesses **MUST** verify that the selected -`ucp.version` and capability versions satisfy the declared constraints during -schema resolution. Incompatible extensions are excluded from the active -capability set (see [Resolution Flow](#resolution-flow)). If `requires` is -absent, the extension is assumed to be compatible with the versions declared -by the profile. +##### Transport Bindings -#### Schema Resolution Convention +=== "REST" -To validate payloads, implementations resolve extension schemas as follows: + **Discovery Failure (424):** -1. Determine the root capability from the operation (e.g., checkout operations - use `dev.ucp.shopping.checkout`) -2. For each active extension, resolve and apply its `$defs[{root_capability}]` + ```http + HTTP/1.1 424 Failed Dependency + Content-Type: application/json -**Example:** A checkout response includes the discount extension. + { + "code": "profile_unreachable", + "content": "Unable to fetch agent profile: connection timeout", + "continue_url": "https://merchant.com/cart" + } + ``` -- Root capability: `dev.ucp.shopping.checkout` -- Extension schema: `discount.json` -- Resolve: `discount.json#/$defs/dev.ucp.shopping.checkout` + **Version Unsupported (422):** -#### Resolution Flow + ```http + HTTP/1.1 422 Unprocessable Content + Content-Type: application/json -Platforms **MUST** resolve schemas following this sequence: + { + "code": "version_unsupported", + "content": "Protocol version 2026-01-12 is not supported. This business supports versions 2026-01-11 and 2026-01-23.", + "continue_url": "https://merchant.com/cart" + } + ``` -1. **Discovery**: Fetch business profile from `/.well-known/ucp` -2. **Negotiation**: Compute capability intersection (see - [Intersection Algorithm](#intersection-algorithm)) -3. **Schema Fetch**: Fetch base schema and all active extension schemas -4. **Version Compatibility**: For each fetched extension schema, - if `requires` is present, verify the negotiated protocol version - and capability versions satisfy the declared constraints. Exclude - incompatible extensions and re-prune orphaned extensions - (steps 3-4 of the [Intersection Algorithm](#intersection-algorithm)) -5. **Compose**: Merge schemas via `allOf` chains based on active extensions -6. **Validate**: Validate requests and responses against the composed schema + **Capabilities Incompatible (200):** -### Profile Structure + ```http + HTTP/1.1 200 OK + Content-Type: application/json -Profile documents are machine-readable discovery documents. Businesses publish -their profile at `/.well-known/ucp`; platforms publish their profile at the URI -advertised in `UCP-Agent`. + { + "ucp": { + "version": "{{ ucp_version }}", + "status": "error", + "capabilities": {} + }, + "messages": [ + { + "type": "error", + "code": "capabilities_incompatible", + "content": "No compatible capabilities in the intersection", + "severity": "unrecoverable" + } + ], + "continue_url": "https://merchant.com" + } + ``` -A profile document is a JSON object with a required `ucp` member. The `ucp` -member contains protocol metadata: protocol version, services, optional -capabilities, and payment handlers. + **Protocol Error — Rate Limit (429):** -For both business and platform profiles, `ucp.version`, `ucp.services`, and -`ucp.payment_handlers` are required. The `services` and `payment_handlers` -registries **MUST** be present even when empty. `ucp.capabilities` is optional -and **MAY** be omitted, though useful commerce profiles normally advertise at -least one capability. + ```http + HTTP/1.1 429 Too Many Requests + Retry-After: 60 + ``` -Profiles **MAY** include public JSON Web Keys used for HTTP Message -Signatures and signed webhooks. When a profile publishes signing keys, -they **MUST** appear in the top-level `keys[]` array — the canonical -UCP profile field that every UCP verifier reads. `keys[]` is a JWK Set -per [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517), so the -same document is simultaneously a UCP profile and a valid JWK Set — -which a signer can reuse as its Web Bot Auth key source. See -[Deployment Patterns for WBA Interop](#deployment-patterns-for-wba-interop) -below. + **Protocol Error — Unauthorized (401):** -Adding, rotating, or removing a key updates this single array. Removal -is the security-critical case: a revoked or compromised key is not -effectively revoked until it is absent from `keys[]`. + ```http + HTTP/1.1 401 Unauthorized + WWW-Authenticate: Bearer realm="ucp" + ``` -UCP defines two well-known key types: **EC** (ECDSA P-256, P-384) and -**OKP** (EdDSA Ed25519); the key-type, curve, and algorithm -vocabularies are open and verifiers skip keys they do not recognize. -See [Message Signatures](../signatures.md) for key format, algorithms, -lookup, and rotation. + Protocol errors use standard HTTP status codes and headers. Response bodies + are optional. -#### Business Profile +=== "MCP" -Businesses publish their profile at `/.well-known/ucp`. An example: + **Discovery Failure (JSON-RPC error):** - -```json -{ - "ucp": { - "version": "{{ ucp_version }}", - "services": { - "dev.ucp.shopping": [ - { - "version": "{{ ucp_version }}", - "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview/", - "transport": "rest", - "endpoint": "https://business.example.com/ucp/v1", - "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/rest.openapi.json" - }, - { - "version": "{{ ucp_version }}", - "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview/", - "transport": "mcp", - "endpoint": "https://business.example.com/ucp/mcp", - "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/mcp.openrpc.json" - }, - { - "version": "{{ ucp_version }}", - "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview/", - "transport": "a2a", - "endpoint": "https://business.example.com/.well-known/agent-card.json" - }, - { - "version": "{{ ucp_version }}", - "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview/", - "transport": "embedded", - "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/embedded.openrpc.json" - } - ] - }, - "capabilities": { - "dev.ucp.shopping.checkout": [ - { - "version": "{{ ucp_version }}", - "spec": "https://ucp.dev/{{ ucp_version }}/specification/shopping/checkout", - "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/checkout.json" - } - ], - "dev.ucp.shopping.fulfillment": [ - { - "version": "{{ ucp_version }}", - "spec": "https://ucp.dev/{{ ucp_version }}/specification/shopping/extensions/fulfillment", - "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/fulfillment.json", - "extends": "dev.ucp.shopping.checkout" - } - ], - "dev.ucp.shopping.discount": [ - { - "version": "{{ ucp_version }}", - "spec": "https://ucp.dev/{{ ucp_version }}/specification/shopping/extensions/discount", - "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/discount.json", - "extends": "dev.ucp.shopping.checkout" + + ```json + { + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -32001, + "message": "UCP discovery failed", + "data": { + "code": "profile_unreachable", + "content": "Unable to fetch agent profile: connection timeout", + "continue_url": "https://merchant.com/cart" } - ], - "dev.ucp.common.identity_linking": [ - { - "version": "{{ ucp_version }}", - "spec": "https://ucp.dev/{{ ucp_version }}/specification/common/identity-linking/", - "schema": "https://ucp.dev/{{ ucp_version }}/schemas/common/identity_linking.json", - "config": { - "providers": { - "com.example.idp": [ - { "type": "oauth2", "auth_url": "https://accounts.example.com/" } - ] - }, - "scopes": { - "dev.ucp.shopping.order:read": {}, - "dev.ucp.shopping.order:manage": {} - } - } + } + } + ``` + + **Version Unsupported (JSON-RPC error):** + + + ```json + { + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -32001, + "message": "Protocol version not supported", + "data": { + "code": "version_unsupported", + "content": "Protocol version 2026-01-12 is not supported. This business supports versions 2026-01-11 and 2026-01-23.", + "continue_url": "https://merchant.com/cart" } - ] - }, - "payment_handlers": { - "com.example.processor_tokenizer": [ - { - "id": "processor_tokenizer", - "version": "{{ ucp_version }}", - "spec": "https://example.com/specs/payments/processor_tokenizer", - "schema": "https://example.com/specs/payments/merchant_tokenizer.json", - "available_instruments": [ + } + } + ``` + + **Capabilities Incompatible (JSON-RPC result):** + + + ```json + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "structuredContent": { + "ucp": { + "version": "{{ ucp_version }}", + "status": "error" + }, + "messages": [ { - "type": "card", - "constraints": { - "properties": { "brand": { "enum": ["visa", "mastercard", "amex"] } } - } + "type": "error", + "code": "capabilities_incompatible", + "content": "No compatible capabilities in the intersection", + "severity": "unrecoverable" } ], - "config": { - "type": "CARD", - "tokenization_specification": { - "type": "PUSH", - "parameters": { - "token_retrieval_url": "https://api.psp.example.com/v1/tokens" - } - } - } - } - ] - } - }, - "keys": [ - { - "kid": "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U", - "kty": "OKP", - "crv": "Ed25519", - "x": "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs", - "use": "sig", - "alg": "EdDSA" - }, - { - "kid": "business_2025", - "kty": "EC", - "crv": "P-256", - "x": "qIVYZVLCrPZHGHjP17CTW0_-D9Lfw0EkjqF7xB4FivA", - "y": "Mc4nN9LTDOBhfoUeg8Ye9WedFRhnZXZJA12Qp0zZ6F0", - "use": "sig", - "alg": "ES256" + "continue_url": "https://merchant.com" + }, + "content": [ + {"type": "text", "text": "{\"ucp\":{…},…}"} + ] + } } - ] -} -``` - -The business profile advertises the business's available transports, -capabilities, payment handlers, and public verification keys. This -example publishes signing keys in the canonical top-level `keys[]` -array (an RFC 7517 JWK Set), so the same document is also a valid JWK -Set — reusable as a Web Bot Auth key source. Every UCP verifier reads -`keys[]`, whether it resolved the key via `UCP-Agent` or via -`Signature-Agent`. + ``` -A WBA-shape verifier reads `keys[]` from this profile **only when the -`Signature-Agent` header selects it** with `type=jwks_uri` (or `type=cimd`) -pointing at the profile URL. The default `type=directory` (when `type` is -omitted) instead expects a *signed* directory document at -`/.well-known/http-message-signatures-directory`, not a static profile, so -it will not read `keys[]` from a static `/.well-known/ucp`. See -[Deployment Patterns for WBA Interop](#deployment-patterns-for-wba-interop). + **Protocol Error — Rate Limit (JSON-RPC error):** -This example uses two keys. Whether a deployment needs one or two depends -on the algorithms its counterparties accept — many need only one; see -[Signature Algorithms](../signatures.md#signature-algorithms). The two keys -here: + + ```json + { + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -32000, + "message": "Rate limit exceeded", + "data": { + "retry_after": 60 + } + } + } + ``` -- An **Ed25519** key (OKP) for HTTP transport identity, WBA-compatible. - The `kid` is the JWK SHA-256 Thumbprint per RFC 7638. -- An **ECDSA P-256** key (EC) for AP2 mandate signing - (`ap2.merchant_authorization`). + **Protocol Error — Unauthorized (JSON-RPC error):** -A business that does not interact with AP2 or WBA may publish a single -ES256 key in `keys[]` (the universal baseline). See -[Key Discovery](#key-discovery) for key lookup and resolution, -[Deployment Patterns for WBA Interop](#deployment-patterns-for-wba-interop) -for hosting choices, and [Message Signatures](../signatures.md) for -signing mechanics. + + ```json + { + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -32000, + "message": "Unauthorized" + } + } + ``` -Businesses that support older protocol versions **SHOULD** include a -`supported_versions` object mapping each older version to a -version-specific profile URI. See [Protocol Version](#protocol-version) -for details. + When using Streamable HTTP transport, servers **MUST** return the + corresponding HTTP status code (e.g., `429` for rate limit) alongside + the JSON-RPC error. The HTTP status code is the primary signal for + error type. -#### Platform Profile +#### Capability Declaration in Responses -Platform profiles are similar and include signing keys for capabilities -requiring cryptographic verification. Capabilities **MAY** include a `config` -object for capability-specific settings (e.g., callback URLs, feature flags). An -example: +The `capabilities` registry in responses indicates active capabilities: - + ```json { "ucp": { "version": "{{ ucp_version }}", - "services": { - "dev.ucp.shopping": [ - { - "version": "{{ ucp_version }}", - "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview/", - "transport": "rest", - "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/rest.openapi.json", - "endpoint": "https://platform.example.com/ucp/v1" - } - ] - }, "capabilities": { "dev.ucp.shopping.checkout": [ - { - "version": "{{ ucp_version }}", - "spec": "https://ucp.dev/{{ ucp_version }}/specification/shopping/checkout", - "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/checkout.json" - } + {"version": "{{ ucp_version }}"} ], "dev.ucp.shopping.fulfillment": [ - { - "version": "{{ ucp_version }}", - "spec": "https://ucp.dev/{{ ucp_version }}/specification/shopping/extensions/fulfillment", - "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/fulfillment.json", - "extends": "dev.ucp.shopping.checkout" - } - ], - "dev.ucp.shopping.order": [ - { - "version": "{{ ucp_version }}", - "spec": "https://ucp.dev/{{ ucp_version }}/specification/shopping/order", - "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/order.json", - "config": { - "webhook_url": "https://platform.example.com/webhooks/ucp/orders" - } - } - ], - "dev.ucp.common.identity_linking": [ - { - "version": "{{ ucp_version }}", - "spec": "https://ucp.dev/{{ ucp_version }}/specification/common/identity-linking/", - "schema": "https://ucp.dev/{{ ucp_version }}/schemas/common/identity_linking.json" - } + {"version": "{{ ucp_version }}"} ] }, "payment_handlers": { - "com.google.pay": [ - { - "id": "gpay_1234", - "version": "2024-12-03", - "spec": "https://developers.google.com/merchant/ucp/guides/gpay-payment-handler", - "schema": "https://pay.google.com/gp/p/ucp/2026-01-11/schemas/gpay_config.json" - } - ], - "dev.shopify.shop_pay": [ - { - "id": "shop_pay_1234", - "version": "{{ ucp_version }}", - "spec": "https://shopify.dev/ucp/shop-pay-handler", - "schema": "https://shopify.dev/ucp/schemas/shop-pay-config.json", - "available_instruments": [ - {"type": "shop_pay"} - ] - } - ], "com.example.processor_tokenizer": [ - { - "id": "processor_tokenizer", - "version": "{{ ucp_version }}", - "spec": "https://example.com/specs/payments/processor_tokenizer-payment", - "schema": "https://example.com/schemas/payments/delegate-payment.json", - "available_instruments": [ - {"type": "card", "constraints": {"properties": {"brand": {"enum": ["visa", "mastercard"]}}}} - ] - } + {"id": "processor_tokenizer", "version": "{{ ucp_version }}", "available_instruments": [{"type": "card"}]} ] } }, - "keys": [ - { - "kid": "platform_2025", - "kty": "EC", - "crv": "P-256", - "x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", - "y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", - "use": "sig", - "alg": "ES256" - } - ] + "id": "checkout_123", + "status": "incomplete", + "currency": "USD", + "line_items": [ ... ], + "totals": [ ... ], + "links": [ ... ] } ``` -### The `ucp` Protocol Namespace +#### Response Capability Selection + +Businesses **MUST** include in `ucp.capabilities` only the capabilities that are: + +1. In the negotiated intersection for this session, AND +2. Relevant to this response's operation type + +**Root Capability Relevance:** + +A root capability is relevant if it matches the operation type: + +- `create_checkout` / `update_checkout` / `complete_checkout` → + `dev.ucp.shopping.checkout` +- `create_cart` / `update_cart` → `dev.ucp.shopping.cart` +- Order webhooks → `dev.ucp.shopping.order` + +**Extension Relevance:** + +An extension is relevant if **any** of its `extends` values matches a relevant +root capability. + +**Selection Examples:** + +| Response Type | Includes | Does NOT Include | +| ------------- | ------------------------------- | ---------------------------- | +| Checkout | checkout, discount, fulfillment | cart, order | +| Cart | cart, discount | checkout, fulfillment, order | +| Order | order | checkout, cart, discount | + +## Identity & Authentication + +UCP profiles serve dual purpose: they declare a party's **capabilities** +for negotiation (see [Profile Structure](#profile-structure)) and publish +**signing keys** for identity verification — enabling both capability +negotiation and cryptographic authentication from a single document. + +Businesses publish their profile at `/.well-known/ucp` as the discovery +entry point — platforms fetch it to determine protocol support, locate +endpoints, and negotiate capabilities. Platforms advertise their profile +URL per-request via the `UCP-Agent` header, enabling businesses to +negotiate capabilities and verify identity. This design enables +**permissionless onboarding** — any platform with a discoverable profile +can interact with any business without prior registration. + +**Web Bot Auth interop.** Signers opting into WBA-shape signatures +additionally emit a `Signature-Agent` header advertising their keys. See +[Identity Resolution Algorithm](#identity-resolution-algorithm) for how +verifiers resolve identity and +[Message Signatures — WBA Interop](../signatures.md#wba-interop) for the +signature shape. + +### Authentication Mechanisms + +Businesses **SHOULD** authenticate platforms to prevent impersonation and ensure +message integrity. UCP is compatible with multiple authentication mechanisms: + +- **API Keys** — Pre-shared secrets exchanged out-of-band +- **OAuth 2.0** — Client credentials or other OAuth flows +- **mTLS** — Mutual TLS with client certificates +- **HTTP Message Signatures** — Cryptographic signatures per + [RFC 9421](https://www.rfc-editor.org/rfc/rfc9421) (see + [Message Signatures](../signatures.md) for full specification) + +HTTP Message Signatures enable permissionless onboarding — businesses can +verify platforms by their advertised public keys without negotiating shared +secrets. The other mechanisms require prior credential exchange and imply a +pre-established relationship. + +Business-to-platform webhooks **MUST** be signed. See +[Message Signatures — When Signatures Apply](../signatures.md#when-signatures-apply). + +#### Identity Binding + +Regardless of authentication mechanism, verifiers **MUST** ensure the +authenticated identity is consistent with the `UCP-Agent` header: + +- **HTTP Message Signatures** — The signer's profile (from `UCP-Agent`) is + verified by signature validation; no additional check needed. +- **API keys / OAuth / mTLS** — Verifiers **MUST** confirm the authenticated + principal is authorized to act on behalf of the profile identified in + `UCP-Agent`. Reject requests where the authenticated identity and claimed + profile conflict. + +### Key Discovery + +Both parties publish public keys in their UCP profile. Platforms fetch +the business profile at `/.well-known/ucp`; businesses fetch the +platform profile from the `UCP-Agent` header (or `Signature-Agent` +header when Web Bot Auth interop is in use). The same profile that +provides capabilities also provides verification keys — this is UCP's +key resolution mechanism for +[RFC 9421](https://www.rfc-editor.org/rfc/rfc9421) HTTP Message +Signatures. + +See [Profile Structure](#profile-structure) for the publishing +contract (the canonical top-level `keys[]` JWK Set). Both resolution +paths read the same list: + +- **Resolved via `UCP-Agent`** (default UCP key lookup) — read `keys[]`. +- **Resolved via `Signature-Agent`** (Web Bot Auth, optional) — read + `keys[]`; the `cimd`/`directory` variants reach the JWK Set through + their own documents. + +For the full verifier algorithm — capability-based key resolution, +profile fetching, and covered-component enforcement — see +[Identity Resolution Algorithm](#identity-resolution-algorithm) below. +For key format (JWK), supported algorithms, key rotation procedures, +and the Web Bot Auth interop signature shape, see +[Message Signatures](../signatures.md). + +### Profile Requirements + +#### Hosting + +Profiles, and the schema and transport-description artifacts they reference, +must be reliably hosted. An unreliable or misconfigured endpoint may prevent +the other party from processing requests. + +1. Published artifacts **MUST** be served over HTTPS. +2. Profile endpoints **MUST NOT** use redirects (3xx). +3. Published artifacts **MUST** include a `Cache-Control` header with + `public` and `max-age` of at least 60 seconds, and **MUST NOT** be + served with `private`, `no-store`, or `no-cache` directives. +4. Published artifacts **SHOULD** include a validator (`ETag` or + `Last-Modified`) so consumers can revalidate cached copies efficiently. + +Profiles represent a party's stable identity and capabilities. Profile +URLs are expected to remain consistent across requests and not contain +per-transaction or per-session configuration — the caching policy above +enforces this by requiring shared cache support with a minimum TTL. + +#### Fetching + +Businesses fetch platform profiles to perform capability negotiation and +verify identity. UCP defines best practices that enable permissionless +onboarding, but businesses retain full control over their access policies +and **MAY** enforce additional rules based on established trust, observed +behavior, or operational requirements. + +Businesses **SHOULD** maintain a registry of pre-approved platforms — +platforms whose profiles have been validated and whose trust is +established through out-of-band mechanisms (API key, OAuth credential, +mTLS certificate, or prior vetting). Known platforms can be served +efficiently based on cached identity and capabilities, and are not +subject to discovery budget constraints. + +When a platform is *not recognized*, it triggers dynamic profile +discovery. Businesses **SHOULD** establish a fixed +discovery footprint so that resource consumption for resolving +unrecognized platforms remains constant regardless of how many platforms +request access. Strategies include: + +- **Fixed-size profile cache** (e.g., LRU) — bounds memory regardless of + the number of unique profile URLs encountered +- **Global rate limit** on discovery fetches — bounds outbound network + without requiring per-origin state tracking +- **Backoff on repeated failures** — reduces retries to persistently + unavailable or malicious profile endpoints +- **Asynchronous discovery** — defer profile resolution by responding + with a `503` status code and `Retry-After` header, and resolve the + profile in the background; when the platform retries, the validated + profile is cached and capability negotiation proceeds synchronously + +These rules apply to any URL dereferenced during identity resolution — +the profile, and any `jwks_uri` or CIMD document a verifier follows: -The member name `ucp` is reserved as the **protocol namespace** in every -structured UCP object scope — an object whose members are schema-defined -fields. The top-level `ucp` member that profiles and responses carry — -described in [Profile Structure](#profile-structure) above — is not a special -wrapper; it is the root manifestation of this reservation: a reserved member -of the root object. The reservation does not apply to a dictionary container, -whose keys are data rather than fields. A dictionary key named `ucp` is -ordinary data. A structured object used as a dictionary value remains an -eligible scope. Schema authors **MUST NOT** define a domain field named `ucp` -in structured object schemas or extensions. +1. Implementations **MUST** reject URLs not served over HTTPS. +2. Implementations **MUST NOT** follow redirects (3xx). +3. Implementations **SHOULD** enforce connect and response timeouts. +4. Implementations **SHOULD** cache profiles with a minimum TTL floor + of 60 seconds, regardless of the origin's `Cache-Control` headers. +5. Implementations **MAY** refresh profiles asynchronously using + stale-while-revalidate semantics. +6. On signature verification failure with an unknown `kid`, + implementations **SHOULD** force-refresh the cached profile once — + but **MUST NOT** do so more than once per TTL floor per origin. +7. Implementations **MUST** reject URLs that resolve to special-use IP + addresses ([RFC 6890](https://www.rfc-editor.org/rfc/rfc6890) — + loopback, link-local including the cloud-metadata address + `169.254.169.254`, private, and other reserved ranges), except a + loopback target when the verifier itself runs on the same loopback + interface (local development). Verifiers **SHOULD** validate the + resolved address, not just the hostname (to resist DNS rebinding), + and **SHOULD NOT** dereference a URL contained within a fetched + document (e.g. a CIMD `jwks_uri`) that resolves to such an address. +8. Implementations **SHOULD** bound the response body size to prevent + unbounded-response resource exhaustion. A UCP profile is an + identity/capability manifest, not a data payload (documented profiles + are under 5 KiB); since the schema sets no size limit, this bound is a + deployment guard, and verifiers **SHOULD** set it no lower than + 128 KiB so it does not reject conformant profiles. -At each eligible structured scope, `ucp` carries the protocol's statements -about that scope: protocol metadata at the root (version, services, -capabilities, payment handlers) and structural annotations such as -[`map_order`](#map_order). +If a profile cannot be fetched (timeout, DNS failure, 5xx) or fails +validation (invalid schema, signing keys, signature mismatch), +businesses **MUST** reject the request with an appropriate error and +status code (see [Error Handling](#error-handling)). -**Openness.** The `ucp` container is open. Consumers **MUST** ignore members -inside `ucp` that they do not recognize (tolerant reader). Openness exists so -documents produced under a newer UCP version remain readable by older -consumers — it is *not* extension space. Only UCP core defines members inside -`ucp`, and extension authors **MUST NOT** place extension data there. An -unrecognized member inside `ucp` means "defined by a newer UCP version," -never "extension data." +### Deployment Patterns for WBA Interop -**No direct recursion.** Producers **MUST NOT** emit a `ucp` member as a direct -child of another `ucp` member (`ucp.ucp`). If one is present, a receiving -Business or Platform **MUST NOT** interpret it as another protocol namespace and -**MUST** ignore that child. Structured objects beneath the namespace, such as a -capability's `config`, remain eligible for their own `ucp` member. +A UCP profile carrying a top-level `keys[]` array is a valid RFC 7517 +JWK Set, which a signer can optionally reuse as its Web Bot Auth key +source. The `Signature-Agent` header's `type` parameter selects +how a verifier resolves the advertised keys. The parameter and +its `jwks_uri`/`cimd`/`directory` values are defined in §4.1 of +[draft-meunier-webbotauth-httpsig-directory-00](https://datatracker.ietf.org/doc/draft-meunier-webbotauth-httpsig-directory/00/). +Each variant can stand alone or point back at the UCP profile: -**Ambient vocabulary.** The protocol namespace is ambient within structured -UCP objects: a Business or Platform **MAY** include a `ucp` member at any -eligible structured scope, and its contents are defined exclusively by UCP -core's vocabulary — the member is part of the UCP document grammar, like the -name reservation itself. The reservation stops at a dictionary container. A -Business or Platform **MUST NOT** interpret a dictionary key named `ucp` as -the protocol namespace; the key and its value are ordinary dictionary data. -For example, `attribution` is a dictionary of string values, so an attribution -key named `ucp` is ordinary attribution data, not a protocol-namespace member. -Guidance for schema authors on working within this reservation lives in the -Schema Authoring Guide's -[The Reserved `ucp` Member](/documentation/schema-authoring/#the-reserved-ucp-member) -section. A Business or Platform encountering a `ucp` member at an eligible -structured scope processes the members it recognizes, each per its own -definition, and **MUST** ignore unrecognized members (see *Openness* above). -A member is admitted to the vocabulary only if it is safe to ignore: a -Business or Platform that does not process it loses only that member's -benefit, never correctness. A Business or Platform that ignores `map_order`, -for example, simply traverses the map unordered — the status quo before -ordering existed. +- **`type=jwks_uri`** — the member value is a JWK Set URL, fetched + directly. Point it at the UCP profile URL and the profile's `keys[]` + serves as the JWK Set: one document is both profile and key source. + Integrity derives from TLS to the profile origin, with no per-key + self-signature. Set `type=jwks_uri` explicitly: omitting `type` + defaults to `directory` (below), which expects a signed + directory, not a static profile. -**Scope determines obligations.** At the root of profiles and responses, the -`ucp` envelope additionally carries the required protocol metadata exactly as -specified elsewhere in this document — this section changes none of those -obligations. A Business or Platform **MAY** omit the member at every other -eligible structured scope. Dictionary containers are not eligible scopes and -carry no protocol-namespace obligation. Conformance to the vocabulary is -defined by this specification's processing rules, not by ordinary instance -validation against open UCP source schemas; that validation treats ambient -`ucp` members as ignored unknown objects. + ```text + Signature-Agent: sig1="https://platform.example/.well-known/ucp";type=jwks_uri + ``` -**Vocabulary applicability.** Each registered protocol-namespace member defines -the document contexts and message directions where it applies. +- **`type=cimd`** — the member value is a Client ID Metadata Document + ([draft-ietf-oauth-client-id-metadata-document](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/)) + whose `jwks_uri` **MAY** point back at the UCP profile. Use when a + counterparty consumes CIMD-shaped agent identity. -**Schema processing.** UCP source schemas are open by default, so ordinary -validation against them may accept ambient `ucp` without applying the protocol -vocabulary. For a selected message direction, a UCP-aware resolver **MUST** -produce a resolved schema that recognizes and validates ambient `ucp` at every -eligible structured scope against the central vocabulary in -`ucp.json#/$defs/members`, subject to that vocabulary's applicability in the -selected direction. The `ucp` namespace remains open to unrecognized members -for forward compatibility, even if the resolved schema rejects other unknown -domain fields. The result is ordinary JSON Schema that standard validators and -code generators can consume. + ```text + Signature-Agent: sig1="https://platform.example/agent";type=cimd + ``` -#### `map_order` +- **`type=directory`** *(default)* — when `type` is omitted or set to + `directory`, the member value is an **origin** (not a full URL): the + verifier appends the registered well-known path + (`/.well-known/http-message-signatures-directory`) to that origin and + fetches a signed directory there. Its format, per-key self-signatures, + and media type are defined by the directory draft §5.2. -JSON object members are unordered: member order is not guaranteed to survive -parsing, and -[RFC 8785](https://www.rfc-editor.org/rfc/rfc8785.html){ target="_blank" } -(JSON Canonicalization Scheme), which UCP signing relies on, sorts object -member names while preserving array element order. `map_order` uses an array -so its declared order survives canonicalization and signing. + ```text + Signature-Agent: sig1="https://platform.example" # type omitted -> directory + Signature-Agent: sig1="https://platform.example";type=directory # explicit + ``` -`map_order` declares a preferred key-traversal order for map-valued fields in -the scope annotated by its containing `ucp` member. At a nested scope, each -key of `map_order` names a map field on the object that contains `ucp`. At the -document root, each key instead names a sibling map field inside the root `ucp` -envelope. Root domain fields outside `ucp`, such as a checkout response's -`actions`, are not targets. Each value is an array of the target map's keys in -preferred traversal order. +### Identity Resolution Algorithm + +UCP and Web Bot Auth define two key-resolution mechanisms. Which one a +verifier uses is chosen by **verifier capability and the headers +present**, not by the signature's `tag` — the `tag` is a hint, not a +gate. Default UCP key lookup (`UCP-Agent`) is supported by every UCP +verifier and works for any UCP signature; Web Bot Auth key lookup +(`Signature-Agent`) is an optional, additive layer. + +A request MAY carry multiple signatures per +[RFC 9421 §4.3](https://www.rfc-editor.org/rfc/rfc9421#section-4.3). +Verifiers attempt each signature independently; the request is +authenticated when at least one signature verifies. The algorithm +below processes a single signature. + +1. **Resolve the signing key.** A verifier uses a resolution mechanism + it supports whose header is present: + - **`UCP-Agent` — default UCP key lookup, supported by every UCP + verifier.** Resolve the `UCP-Agent` profile URL and read + `keys[]`. This path applies to UCP signatures that are + untagged (default UCP) or carry `tag="web-bot-auth"` (the + dual-audience shape); the verifier resolves them via `UCP-Agent`, + treats `signature-agent` as an ordinary covered component, and + need not implement Web Bot Auth key discovery. (Verifying a + dual-audience signature does still require supporting the key's + algorithm — whichever the signer used, per + [Signature Algorithms](../signatures.md#signature-algorithms) — and + RFC 9421 §2.1.2 Dictionary-member component selection to cover + `signature-agent;key="