Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 111 additions & 39 deletions docs/specification/fulfillment.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,22 @@
## Overview

The fulfillment extension enables businesses to advertise support for physical
goods fulfillment (shipping, pickup, etc).

This extension adds a `fulfillment` field to Checkout and/or Catalog:

* **Checkout** (`dev.ucp.shopping.checkout`) — selection and cost: which
items go where, by which method, at what price and ETA.
goods fulfillment (shipping, pickup, etc). Each parent capability opts in
independently, with a parent-specific projection:

* **Checkout** (`dev.ucp.shopping.checkout`) — location-aware context plus
selection and cost: which items go where, by which method, at what price
and ETA.
* **Cart** (`dev.ucp.shopping.cart`) — location-aware context only; it does not
add methods, destinations, groups, options, or selection IDs.
* **Catalog** (`dev.ucp.shopping.catalog.search` and
`dev.ucp.shopping.catalog.lookup`) — discovery: a variant advertises the
fulfillment options available for it, based on the provided buyer
context. See [Catalog Discovery](#catalog-discovery).
`dev.ucp.shopping.catalog.lookup`) — location-aware request context and
discovery: a variant advertises the fulfillment options available for it.
See [Catalog Discovery](#catalog-discovery).

On Checkout, the `fulfillment` field contains:

* `methods[]` — fulfillment methods applicable to cart items (shipping, pickup, etc.)
* `methods[]` — fulfillment methods applicable to line items (shipping, pickup, etc.)
* `line_item_ids` — which items this method fulfills
* `destinations[]` — where to fulfill (address, store location)
* `groups[]` — business-generated packages, each with selectable `options[]`
Expand All @@ -53,6 +55,55 @@ On Checkout, the `fulfillment` field contains:
* `selected_option_id` = `options[0].id` 🔘✅ In-Store Pickup
* `options[1]` 🔘 Curbside Pickup

### Location Context

Fulfillment adds an optional `location` to the parent `context`. `location` is
accepted on Catalog requests and on Cart and Checkout Create and Update
requests.

These fields have distinct semantic roles; they are not precision levels of the
same location:

| Field | Role |
| --- | --- |
| `context.location` | Provisional, non-binding Business-location anchor for location-scoped pricing, availability, estimates, or initial options—roughly, “evaluate this experience at this Business location.” |
| Catalog `filters.fulfills_to` | Explicit fulfillment destination and filter: “can this be fulfilled to here?” |
| Catalog response `variants[].fulfillment.methods[].location` | Business location for which the availability of a place-based method was resolved. |
| Checkout `selected_destination_id` or authoritative fulfillment address | Explicit destination for the affected fulfillment scope; it supersedes `context.location` there. |

**Continuity example.** The same Business-scoped Location Identifier can appear
in different fields without collapsing their roles. Catalog may report a
place-based method at `loc_123`. A Platform continuing that location-scoped
experience can carry `loc_123` as Cart `context.location`. Cart-to-Checkout
conversion carries `context.location` into Checkout, allowing the Business to
surface that location as the initial fulfillment choice. Once the Platform
selects it through `selected_destination_id`, it becomes the explicit
destination for the affected fulfillment scope.

#### Platform

* A Platform **MUST NOT** send a location ID established by one Business to a
different Business. It **MAY** provide an ID learned from a Fulfillment
response or another mechanism operated by the same Business.
* A Platform **MUST NOT** treat `context.location` as a binding fulfillment
destination.

#### Business

* A Business **MUST** ignore an unknown or inapplicable `context.location` and
return a successful response for an otherwise valid request. It **MAY**
report the ignored value through a warning in `messages[]`.
* Catalog confirms resolution through the applicable
`variants[].fulfillment.methods[].location`; top-level response context is
not part of this extension's Catalog response contract.
* For Catalog fulfillment destination and availability resolution, a Business
**MUST** use explicit `filters.fulfills_to` instead of
`context.address_country`, `context.address_region`, `context.postal_code`,
or `context.location`. For the Checkout scope governed by an
explicit `selected_destination_id` or authoritative fulfillment address,
the Business **MUST** use that destination or address instead of
`context.location`.

## Schema

Fulfillment applies only to items requiring physical delivery. Items not
Expand All @@ -69,6 +120,10 @@ method.

{{ schema_fields('types/fulfillment_resp', 'fulfillment') }}

#### Fulfillment Context

{{ extension_schema_fields('fulfillment.json#/$defs/fulfillment_context', 'fulfillment') }}

#### Fulfillment Method

{{ schema_fields('types/fulfillment_method_resp', 'fulfillment') }}
Expand Down Expand Up @@ -312,10 +367,11 @@ method has:
* `availability` — whether the variant is available via this method at the
specified or inferred location.
* `location` — for place-based methods (e.g. `pickup`), the resolved
location id, and the business's stable identifier for that location. A
business that advertises pickup at a `location` MUST accept the same id
as `selected_destination_id` for that method, so a discovered location
can be used in cart and checkout.
[Location Identifier](glossary.md#commerce) for that method. Across UCP
surfaces exposed by the same Business, the same value identifies the same
location; the containing field defines that location's role. A Business
that advertises pickup at a `location` **MUST** accept that same ID as
`selected_destination_id` for that method in Checkout.
* `options` — concrete fulfillment choices within this method (e.g.
Standard, Express); see [Options](#options). Optional.

Expand All @@ -334,8 +390,8 @@ variant-level value.
A method MAY carry `options[]`, a representative subset of its fulfillment
options — not an exhaustive list. Without a destination or full cart,
catalog SHOULD preview meaningful boundary options for the buyer (e.g.
cheapest, fastest); the full, high-resolution set is negotiated in cart and
checkout once those are known.
cheapest, fastest); the full, high-resolution set is negotiated in checkout
once the line items and destination are known.

Each option carries an `id` and a `title` (a short label distinguishing it
from siblings), plus an optional renderable `description` for context. These
Expand All @@ -346,11 +402,11 @@ none, surfacing only `type`, `description`, and `availability`; options are
nested directly under the method, with no group layer (unlike checkout
`methods[].groups[].options[]`).

A discovered option `id` lets a buyer's choice carry forward: a business
SHOULD accept the same id as `selected_option_id` in cart and checkout.
The id is a best-effort handle, not a guaranteed match — an option
discovered for a single product may differ in a cart, where other
products, quantities, and combined fulfillment modify the options.
A discovered option `id` lets a buyer's choice carry forward to Checkout: a
Business **SHOULD** accept the same ID as `selected_option_id` unless other
products, quantities, or combined fulfillment change the available options.
The ID is a best-effort handle, not a guaranteed match. Cart does not carry
option IDs or other full Fulfillment structures.

### Shapes

Expand All @@ -376,10 +432,14 @@ products, quantities, and combined fulfillment modify the options.

### Location and method: `context` and `filters`

* **`context`** (`address_country` / `address_region` / `postal_code`) is
where the *buyer* is — a non-binding hint the business uses to report
`availability`. On a market-scoped catalog it MAY narrow results;
otherwise it annotates rather than removes them.
* **`context`** provides non-binding hints that a Business uses to report
`availability`. Its coarse locality fields (`address_country` /
`address_region` / `postal_code`) describe the Buyer's locality. Its
`location` is a provisional Business-location anchor for store-scoped
pricing, availability, or initial fulfillment options; it need not be the
Buyer's physical location. On a market-scoped Catalog, the Business
**MAY** use context to narrow results; otherwise, context annotates rather
than removes them.
* **`filters.fulfills_to`** is where the order is *fulfilled to* — a single
destination, named by value (a coarse address: `address_country` /
`address_region` / `postal_code`) or by reference (a `location` id — a
Expand All @@ -391,9 +451,11 @@ products, quantities, and combined fulfillment modify the options.
* **`filters.methods`** restricts results to specific method types (e.g.
`["pickup"]`).

Provide location once: `context` for where the buyer is, `fulfills_to` for
an explicit destination. When both are present, `fulfills_to` supersedes
`context`.
`context` carries non-binding Buyer locality or a provisional
Business-location anchor; `fulfills_to` names an explicit destination. When
both are present, `fulfills_to` supersedes `context.address_country`,
`context.address_region`, `context.postal_code`, or `context.location` for
fulfillment destination and availability resolution.

### Example

Expand Down Expand Up @@ -467,10 +529,17 @@ carries none — `options` is optional.
Businesses and platforms declare fulfillment constraints in their profiles.
Businesses fetch platform profiles to adapt responses accordingly.

The `extends` array lists the capabilities this extension adds fulfillment
to. Checkout is the authoritative, transactional surface; catalog is for
discovery. A business lists the catalog capabilities in `extends` to expose
fulfillment on catalog, or omits them to scope itself to checkout only.
The `extends` array lists the parent capabilities to which this extension
applies. Checkout is the authoritative, transactional surface; catalog is for
discovery; Cart receives only location-aware context. Each parent is an
independent opt-in, so advertising fulfillment for one parent does not activate
it for another.

A Platform or Business that advertises Cart and extends either Catalog
capability with Fulfillment **SHOULD** also extend Cart with Fulfillment to
preserve `context.location` across the Catalog → Cart → Checkout journey.
Omitting the Cart projection means location-scoped Cart continuity is not
provided.

### Platform Profile

Expand All @@ -483,8 +552,8 @@ single-group responses. The response shape is always
`methods[].groups[]`—the difference is whether `groups.length` can exceed 1
within each method.

Default declaration (single group per method; fulfillment surfaced on
checkout and on catalog discovery):
Default declaration (single group per Checkout method; fulfillment surfaced on
Checkout and Catalog, with location context on Cart):

<!-- ucp:example schema=profile def=platform_schema target=$.ucp.capabilities -->
```json
Expand All @@ -496,6 +565,7 @@ checkout and on catalog discovery):
"schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/fulfillment.json",
"extends": [
"dev.ucp.shopping.checkout",
"dev.ucp.shopping.cart",
"dev.ucp.shopping.catalog.search",
"dev.ucp.shopping.catalog.lookup"
]
Expand All @@ -504,8 +574,8 @@ checkout and on catalog discovery):
}
```

A party that does not expose catalog discovery MAY narrow `extends` to
`"dev.ucp.shopping.checkout"` (string form) or to a single-element array.
A Platform or Business **MAY** narrow `extends` to any parent or set of parents
it supports, using the string form for one parent or an array for several.

Opt-in declaration (business MAY return multiple groups per method):

Expand All @@ -519,6 +589,7 @@ Opt-in declaration (business MAY return multiple groups per method):
"schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/fulfillment.json",
"extends": [
"dev.ucp.shopping.checkout",
"dev.ucp.shopping.cart",
"dev.ucp.shopping.catalog.search",
"dev.ucp.shopping.catalog.lookup"
],
Expand All @@ -545,6 +616,7 @@ Businesses declare what fulfillment configurations they support using
"schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/fulfillment.json",
"extends": [
"dev.ucp.shopping.checkout",
"dev.ucp.shopping.cart",
"dev.ucp.shopping.catalog.search",
"dev.ucp.shopping.catalog.lookup"
],
Expand All @@ -559,7 +631,7 @@ Businesses declare what fulfillment configurations they support using
}
```

This example says: shipping can go to multiple addresses, and carts can mix
This example says: shipping can go to multiple addresses, and checkout can mix
shipping+pickup.

### Business Response Behavior
Expand Down Expand Up @@ -610,10 +682,10 @@ like any other method.

**Example — adding `home_installation`.** No schema change or registration is
needed. Emit the value directly as the `type` on catalog and checkout, and
filter with `filters.methods: ["home_installation"]`. For cart and checkout
filter with `filters.methods: ["home_installation"]`. For checkout
negotiation, declare its behavior in the business profile `config` — e.g.
include `["shipping", "home_installation"]` in `method_combinations`
so a cart can mix shipped and installed items (see
so a checkout can mix shipped and installed items (see
[Business Profile](#business-profile)). On a catalog variant's method:

<!-- ucp:example schema=shopping/fulfillment def=catalog_fulfillment_method op=read -->
Expand Down
13 changes: 7 additions & 6 deletions docs/specification/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,13 @@ acronym in each specification Markdown file spells out the full term (e.g.,

## Commerce

| Term | Acronym | Definition |
| :--------------------------- | :------ | :---------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Business** | - | The entity selling goods or services. In UCP, they act as the **Merchant of Record (MoR)**, retaining financial liability and ownership of the order. |
| **Merchant of Record** | MoR | The legal entity responsible for the sale, including financial liability and order ownership. |
| **Payment Service Provider** | PSP | The financial infrastructure provider that processes payments, authorizations, and settlements on behalf of the business. |
| **Platform** | - | The consumer-facing surface (AI agent, app, website) acting on behalf of the user to discover businesses and facilitate commerce. |
| Term | Acronym | Definition |
| :--------------------------- | :------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Business** | - | The entity selling goods or services. In UCP, they act as the **Merchant of Record (MoR)**, retaining financial liability and ownership of the order. |
| **Location Identifier** | ID | An opaque, stable string assigned within a Business namespace. Across UCP surfaces exposed by that Business, the same value identifies the same location; the containing field defines that location's role. |
| **Merchant of Record** | MoR | The legal entity responsible for the sale, including financial liability and order ownership. |
| **Payment Service Provider** | PSP | The financial infrastructure provider that processes payments, authorizations, and settlements on behalf of the business. |
| **Platform** | - | The consumer-facing surface (AI agent, app, website) acting on behalf of the user to discover businesses and facilitate commerce. |

## Payments

Expand Down
7 changes: 7 additions & 0 deletions source/schemas/common/types/location_id.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ucp.dev/schemas/common/types/location_id.json",
"title": "Location ID",
"description": "Opaque, stable identifier for a location assigned within a Business namespace. Across UCP surfaces exposed by that Business, the same value identifies the same location; the containing field defines the role of that location.",
"type": "string"
}
Loading
Loading