Skip to content
Draft
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
181 changes: 167 additions & 14 deletions docs/specification/fulfillment.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,16 @@
The fulfillment extension enables businesses to advertise support for physical
goods fulfillment (shipping, pickup, etc).

This extension adds a `fulfillment` field to Checkout containing:
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.
* **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).

On Checkout, the `fulfillment` field contains:

* `methods[]` — fulfillment methods applicable to cart items (shipping, pickup, etc.)
* `line_item_ids` — which items this method fulfills
Expand Down Expand Up @@ -280,11 +289,134 @@ If the buyer chooses pickup but the platform doesn't support split
fulfillment, the platform **SHOULD** use `continue_url` to hand off to the
business's checkout.

## Catalog Discovery

When the fulfillment extension extends the Catalog capability, each variant
in a catalog response carries a `fulfillment` object listing the fulfillment
methods available for that variant and their availability — so a buyer
browsing the catalog can see how an item can be fulfilled.

### Methods

`fulfillment.methods[]` lists the methods available for a variant. Each
method has:

* `type` — the fulfillment method (e.g. `shipping`, `pickup`); see
[Method Types](#method-types).
* `description` — short, buyer-facing summary of how the variant is
fulfilled via this method (e.g. "Ships in 2–4 business days"). Directly
renderable; see [Rendering](#rendering).
* `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.

Catalog reports availability for a single location per method — the one
specified via `fulfills_to` or inferred from `context`; discovering and
comparing other locations is handled separately.

### Shapes

#### Catalog Fulfillment

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

#### Catalog Fulfillment Method

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

#### Availability

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

#### Fulfillment Destination Filter

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

### 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.
* **`filters.fulfills_to`** is where the order is *fulfilled to* — a
`location` id, or a coarse address (`address_country` / `address_region`
/ `postal_code`). It restricts results to what can be fulfilled there
and seeds method `availability`. It may differ from `context` (e.g. a
gift).
* **`filters.method`** 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`.

### Example

A variant exposes two fulfillment methods: shipping to the buyer's ship-to
and pickup today at a named store. Each method carries its own availability,
and `pickup` references the resolved location by id.

<!-- ucp:example schema=shopping/fulfillment def=fulfillment_search_response op=read -->
```json
{
"ucp": { "version": "{{ ucp_version }}" },
"products": [
{
"id": "prod_kettle",
"title": "Electric Kettle",
"description": { "plain": "1.7L electric kettle." },
"price_range": {
"min": { "amount": 4999, "currency": "USD" },
"max": { "amount": 4999, "currency": "USD" }
},
"variants": [
{
"id": "var_ss",
"title": "Stainless Steel",
"description": { "plain": "Stainless steel finish." },
"price": { "amount": 4999, "currency": "USD" },
"availability": { "available": true, "status": "in_stock" },
"fulfillment": {
"methods": [
{
"type": "shipping",
"description": { "plain": "Ships to your address" },
"availability": { "available": true, "status": "in_stock" }
},
{
"type": "pickup",
"description": { "plain": "Pickup today at Downtown Store" },
"location": "loc_downtown",
"availability": { "available": true, "status": "in_stock" }
}
]
}
}
]
}
]
}
```

Each method is a way the variant can be fulfilled, with its own
`availability`. Each method's `description` is directly renderable, so a
platform can present it without recognizing the `type` (see
[Rendering](#rendering)).

## Configuration

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.

### Platform Profile

Platforms declare their rendering capabilities using `platform_schema`:
Expand All @@ -296,7 +428,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):
Default declaration (single group per method; fulfillment surfaced on
checkout and on catalog discovery):

<!-- ucp:example schema=profile def=platform_schema target=$.ucp.capabilities -->
```json
Expand All @@ -306,12 +439,19 @@ Default declaration (single group per method):
"version": "{{ ucp_version }}",
"spec": "https://ucp.dev/{{ ucp_version }}/specification/fulfillment",
"schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/fulfillment.json",
"extends": "dev.ucp.shopping.checkout"
"extends": [
"dev.ucp.shopping.checkout",
"dev.ucp.shopping.catalog.search",
"dev.ucp.shopping.catalog.lookup"
]
}
]
}
```

A party that does not expose catalog discovery MAY narrow `extends` to
`"dev.ucp.shopping.checkout"` (string form) or to a single-element array.

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

<!-- ucp:example schema=profile def=platform_schema target=$.ucp.capabilities -->
Expand All @@ -322,7 +462,11 @@ Opt-in declaration (business MAY return multiple groups per method):
"version": "{{ ucp_version }}",
"spec": "https://ucp.dev/{{ ucp_version }}/specification/fulfillment",
"schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/fulfillment.json",
"extends": "dev.ucp.shopping.checkout",
"extends": [
"dev.ucp.shopping.checkout",
"dev.ucp.shopping.catalog.search",
"dev.ucp.shopping.catalog.lookup"
],
"config": { "supports_multi_group": true }
}
]
Expand All @@ -344,7 +488,11 @@ Businesses declare what fulfillment configurations they support using
"version": "{{ ucp_version }}",
"spec": "https://ucp.dev/{{ ucp_version }}/specification/fulfillment",
"schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/fulfillment.json",
"extends": "dev.ucp.shopping.checkout",
"extends": [
"dev.ucp.shopping.checkout",
"dev.ucp.shopping.catalog.search",
"dev.ucp.shopping.catalog.lookup"
],
"config": {
"allows_multi_destination": {
"shipping": true
Expand Down Expand Up @@ -375,18 +523,23 @@ shipping+pickup.
* Platform is responsible for rendering group selection UI (e.g., choose
shipping speed per package)

### Adding New Methods
### Method Types

`fulfillment_method.type` (checkout) and `catalog_fulfillment_method.type`
(catalog) share one vocabulary of fulfillment method types. The field is an
open string: consumers ignore values they do not recognize.

Extensions that extend fulfillment with new method types (e.g.,
`local_delivery`) **MUST** add an extension schema that:
**Well-known values:**

1. Adds the method to the `type` enum in `fulfillment_method`
2. Adds corresponding business config options:
* `allows_multi_destination.local_delivery: boolean`
* `allows_method_combinations` items enum (includes `"local_delivery"`)
| Value | Meaning |
| --- | --- |
| `shipping` | Carrier ships to the buyer's address. |
| `pickup` | Buyer picks up at a named location. |

Note: Platform's `supports_multi_group` is method-agnostic (single boolean),
so no extension needed.
**Adding method types.** `type` is an open string: businesses MAY use
additional values, and consumers ignore values they do not recognize. New
well-known values are added via a UCP RFC and a `dev.ucp.shopping.fulfillment`
version bump.

## Examples

Expand Down
22 changes: 22 additions & 0 deletions source/schemas/common/types/locality.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ucp.dev/schemas/common/types/locality.json",
"title": "Locality",
"description": "A coarse geographic location — country, region, and postal code. A lightweight alternative to a full postal address.",
"type": "object",
"additionalProperties": true,
"properties": {
"address_country": {
"type": "string",
"description": "The country, as a 2-letter ISO 3166-1 alpha-2 code (e.g. \"US\"). A 3-letter alpha-3 code or full country name MAY also be used."
},
"address_region": {
"type": "string",
"description": "The first-level administrative region within the country (e.g. a state or province such as California)."
},
"postal_code": {
"type": "string",
"description": "The postal code (e.g. \"94043\")."
}
}
}
Loading
Loading