From a4f90153354d117fe0fdc3689d8816d17e633526 Mon Sep 17 00:00:00 2001 From: amithanda Date: Sat, 12 Sep 2026 02:58:22 -0700 Subject: [PATCH] docs: restore specification overview section order The overview page opened with ~780 lines of deep normative detail (Quantities and units, Request Constraints, Actions) before the reader reached the architectural introduction, because each of those sections was inserted at the top of the file, immediately after "Schema notes": - Actions (#582) inserted at line 34 - Quantities and units (#653) inserted at line 34 - Request Constraints (#655) inserted at line 175 Each insertion was individually reasonable, since these are foundational cross-cutting primitives, but the sequence stacked them ahead of "Discovery, Governance, and Negotiation", which is the actual entry point for a first-time reader. This is visible on /latest but not on /2026-04-08, which predates all three. Move the three sections to sit alongside Policies after "Standard Capabilities", following the precedent set by #572, which placed the first of these cross-cutting primitives there. This restores the opening reading order of the 2026-04-08 release and groups all four core primitives together. Pure reorder: no prose edits and no heading level changes, verified by comparing the sorted line multiset against the parent commit. All 142 heading anchors are preserved, so the 20+ pages that deep-link into #quantities-and-units, #request-constraints and #actions are unaffected. --- docs/specification/overview/index.md | 5084 +++++++++++++------------- 1 file changed, 2542 insertions(+), 2542 deletions(-) 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="