Skip to content

feat(cart): Embedded protocol transport binding for cart capability + reauth mechanism in ECP#244

Draft
jingyli wants to merge 3 commits intoUniversal-Commerce-Protocol:mainfrom
jingyli:clean-cart-embedded
Draft

feat(cart): Embedded protocol transport binding for cart capability + reauth mechanism in ECP#244
jingyli wants to merge 3 commits intoUniversal-Commerce-Protocol:mainfrom
jingyli:clean-cart-embedded

Conversation

@jingyli
Copy link
Contributor

@jingyli jingyli commented Mar 7, 2026

Reopening the PR to keep commit history clean (original PR) and hopefully third-time is the charm. This will also cross-reference/carry-over comments from the old PR.

Summary

Cart capability (dev.ucp.shopping.cart) is being introduced in #73. This adds an additional transport binding (EP) on the capability (building directly upon the referenced PR), on top of existing REST & MCP bindings. It follows the main UCP principle that "Embedded bindings should retain symmetry regardless of the capability it’s implemented for" (and hence why certain special mechanisms like auth are also being introduce to ECP to maintain symmetry).

Motivation

Businesses need a way to embed their cart building UI into eligible platforms, especially when complex experiences (i.e. item recommendations & upsells) are involved during cart building. Embedded Cart Protocol (ECaP) addresses this need.

Goals

  • Enable businesses to embed cart UI into eligible hosts in a secure manner, especially if identity linking is a required pre-requisite for the flow.
  • Establish a new auth exchange mechanism for embedded transport binding that utilizes postMessage mechanisms.
  • Maintain general schema & design consistency with existing ECP (i.e. reuse naming conventions, message types, etc.).
  • Introduce a way for businesses to advertise exactly for which capability is EP active for.

Non-Goals

Detailed Design

Key methods supported by ECaP:

Category Communication Direction Purpose Pattern Core Messages
Handshake Embedded Cart -> Host Establish connection between host and Embedded Cart. Request ect.ready
Authentication Embedded Cart -> Host Communicate auth data exchanges between Embedded Cart and host. Request ect.auth
Lifecycle Embedded Cart -> Host Inform of cart state in Embedded Cart. Notification ect.start, ect.complete
State Change Embedded Cart -> Host Inform of cart field changes. Notification ect.line_items.change, ect.buyer.change, ect.messages.change

Risks and Mitigations

  • Complexity: ECaP introduces another transport mechanism to implement & support. Mitigation: Not all businesses need to support it as part of its services advertisement. Also structurally it's also very similar to ECP so businesses already supporting ECP should be familiar with the design already.
  • Security: Introduced new request mechanism (ect.auth) to exchange required authorization data instead of adding them as query parameters in continue_url to avoid hijacking attacks & support reauth scenarios.
  • Backward Compatibility: None, new transport binding on a new capability.
  • Schema Drift: Not introducing any new schemas, only new methods that utilizes existing schema components.

Graduation Criteria

Working Draft → Candidate:

  • Schema merged and documented (with Working Draft disclaimer).
  • Unit and integration tests are passing.
  • Initial documentation is written.
  • TC majority vote to advance.

Candidate → Stable:

  • Adoption feedback has been collected and addressed.
  • Full documentation and migration guides are published.
  • TC majority vote to advance.

Implementation History

TBD

@igrigorik igrigorik added the TC review Ready for TC review label Mar 10, 2026
@igrigorik igrigorik added this to the Working Draft milestone Mar 10, 2026
Copy link
Contributor

@lemonmade lemonmade left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a few small nits and questions, but I am really liking how this has shaped up, especially the symmetry with Embedded Checkout 👍 This PR does still have one Cart-only message, ect.line_items.change_request; do we think this same feature makes sense in embedded checkout?

- **Result Payload:**
- `authorization` (string, **REQUIRED**): The requested authorization data,
can be in the form of an OAuth token, JWT, API keys, etc.
- `checkout` (object, **REQUIRED**): An optional checkout holding the last known state to the host.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? The embedded checkout should know the last checkout it sent before this message.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question. Omit?

"id": "auth_1",
"method": "ec.auth",
"params": {
"type": "oauth"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the allowed types here?

- **Direction:** host → Embedded Checkout
- **Type:** Response
- **Result Payload:**
- `authorization` (string, **REQUIRED**): The requested authorization data,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, authorization reads a little weirdly for a message called ec.auth. What about using token here, or credential, which mirrors the name for a similar concept in payment instruments?

```

If the ingestion of the authorization is not successful, Embedded Checkout **MAY**
re-initiate this request with the host again.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we should how a Host would indicate failure, and what an Embedded Checkout should do with failures that come up?

in the cart interface. These are informational only. The cart has
already applied the changes and rendered the updated UI.

#### `ect.line_items.change`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are missing documentation for ect.line_items.change_request.

Copy link
Contributor

@igrigorik igrigorik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall 👍 on the feature. A couple authoring and technical flags to jam through...

Why are adding ec.auth / ect.auth?

I understand the reauth scenario (token expiry mid-session) but this feels unlikely and marginal. Why was this not an issue for checkout and problem for cart? I would separate this discussion and functionality out from this PR.

Do we really need config.capabilities?

UCP already has a capability negotiation model: discovery (ucp.capabilities) → per-session intersection (response envelope) → per-message handshake. This PR adds a fourth layer — a capability registry nested inside the embedded transport config.

I think the per-response pattern is sufficient here. When a host creates a cart, it gets back ucp.services[embedded] in the response: it knows ECaP is available for this cart. When you're ready to transition to checkout, checkout response will tell you if embedded checkout is available — all of this works already. The response context disambiguates which capability the embedded transport applies to.

I don't think we need another discovery mechanism here. More on this below...

Complete semantics are underspecified

ec.complete in checkout means "order placed" — it's terminal. ect.complete in cart means "buyer wants to proceed to checkout", which is a transition signal. Same name, different semantics.

Conceptually what we're modelling is closer to ec.payment.credential_request: buyer taps "pay now," business requests what it needs from the host, then proceeds. I think complete is workable but something to think about.

Should we model transition explicitly? As a napkin sketch...

  {
    "method": "ec.cart.complete",
    "params": {
      "cart": { "id": "cart_123", ... },
      "transition": {   // or navigation, ..., whichever
        "checkout": {
          "url": "https://business.com/checkout/from-cart/cart_123",
          "delegate": ["payment.credential", "fulfillment.address_change"]
        }
      }
    }
  }

If transition.checkout is present: business is advertising that it offers embedded checkout at this URL with these delegations. In this contract, host is always in control. It can use the offered transition URL, append ec_* params, navigate current or new frame/webview, or ignore the offer and run create checkout via API call + use native flow. Business offers, host chooses.

This also allows host to control whether it wants to reuse current frame/webview vs. spin up new one -- for example, if it wants to control transition between states. It also eliminates the need for config.capabilities: the transition advertisement IS the per-session embedded checkout signal, delivered at exactly the moment it matters.

There is now a lot of duplicated boilerplate across EC/ECT

Let's think how we can extract and refactor duplicated spec content and schema to ensure we have consistency across these protocols.

To that end, why are redefining same and common mechanisms (e.g. auth parameters) with different names? As an example, ec_auth should be consistent and reusable primitive across all flows; ec_auth is the "embeded commerce auth token". URL params configure the embedded session; they don't care whether you're embedding a cart or a checkout. We shouldn't need to redefine these and create opportunities to deviate across capabilities. This simplifies the implementation dramatically as well. Same argument applies to delegate, color_scheme, etc.

Let's extract the common set of in a shared spec. Then checkout, cart, and whatever other embedded modes we invent, can build on top with just the unique bits they need.

ECT... 😅

Bikeshed: why "T", it's never explained?

In retrospect, maybe we should have namespaced (ec.{capability}.{action}), and should we consider that here and now? Today we have checkout and adding cart, next we'll invent orders (eo?). Instead of proliferating opaque prefixes: ec.cart.{...}, ...?

Embedded Checkout Protocol (ECP) is a checkout-specific implementation of UCP's Embedded Protocol (EP) transport binding that enables a host to embed a business's checkout interface

This is a mess. Here's how I'd clean it up...

Embedded Commerce Protocol (ECP) is a transport binding that enables a host to embed a business capability (e.g. cart, checkout), receive events as buyer interacts, and request and process delegated buyer actions.

In other words, "ECP" is the embedded protocol of UCP. It supports multiple capabilities, and provides shared primitives (auth, delegate, etc), on which capabilities scaffold their specialized request/response semantics.


I recognize that I'm pointing to a larger refactor, but now is the right time to do it as we go from n=1 to multiple embedded capabilities. Happy to help with the refactor.

```json
{
"jsonrpc": "2.0",
"method": "ec.messages.change",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be ect.messages.change?

in the cart interface. These are informational only. The cart has
already applied the changes and rendered the updated UI.

#### `ect.line_items.change`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Echoing @lemonmade: ect.line_items.change_request is defined in embedded.openrpc.json but is not documented. Also: ect.context.change is defined in the OpenRPC but missing from the spec.

- **Result Payload:**
- `authorization` (string, **REQUIRED**): The requested authorization data,
can be in the form of an OAuth token, JWT, API keys, etc.
- `cart` (object, **REQUIRED**): An optional cart holding the last known state to the host.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contradiction: cart is declared (object, **REQUIRED**) in the payload description, but the text says "An optional cart holding the last known state."

}
```

### Loading an Embedded Checkout URL
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be "Loading an Embedded Cart URL"?

completed cart.

- **Direction:** Embedded Cart → host
- **Type:** Request
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ect.complete is documented as a Request (with id: "transition_1"), but in embedded.openrpc.json it has no result schema — which is the notification pattern.

For comparison, ec.complete in ECP is a notification. If ECaP's ect.complete should be a request (which makes sense for the cart→checkout transition handshake), the OpenRPC needs a result schema defining what the host returns (e.g., acknowledgment, checkout initiation data).


### Handshake Messages

#### `ect.ready`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto, this is same handshake as ec.ready: can we extract this and just note the name difference?


### Authentication

#### `ect.auth`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Reauth: Certain authentication methods (i.e. OAuth token) have strict expiration timestamps.
    If a session lasted longer than the allowed duration, business can request for a refreshed
    authorization to be provided by the host before the session continues

True but practically, is this.. a problem we need to solve? What is our expected TTL on these tokens?

}
```

### Security for Web-Based Hosts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto, let's not replicate this. Extract to shared resource.

- **Result Payload:**
- `authorization` (string, **REQUIRED**): The requested authorization data,
can be in the form of an OAuth token, JWT, API keys, etc.
- `checkout` (object, **REQUIRED**): An optional checkout holding the last known state to the host.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question. Omit?

"type": "string",
"description": "Requested authorization. Some common examples include API key and OAuth token."
},
"cart": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checkout?

}
},

{
Copy link

@gsmith85 gsmith85 Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idea: Consolidate to a single ect.change method.

Right now, the host has to run a JSON diff to figure out what triggered the event for several of these methods (e.g. ect.line_items.change). If the contract requires an array of mutated JSON paths alongside the full cart snapshot, we keep the idempotency of the snapshot pattern but eliminate the diffing overhead for the host.

Further, we can continue to be specific about which properties MUST provide change notification now, while providing a flexible API that will avoid needing to add many change* notification RPCs going forward.

{
  "method": "ect.change",
  "params": {
    "changed_paths": [
      "$.line_items[1].quantity", 
      "$.totals"
    ],
    "cart": { ... full snapshot ... }
  }
}


All ECaP parameters are passed via URL query string, not HTTP headers, to ensure
maximum compatibility across different embedding environments. Parameters use
the `ect_` prefix to avoid namespace pollution and clearly distinguish ECaP

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per this observation from @igrigorik: #244 (review) :

In retrospect, maybe we should have namespaced (ec.{capability}.{action}), and should we consider that here and now? Today we have checkout and adding cart, next we'll invent orders (eo?). Instead of proliferating opaque prefixes: ec.cart.{...}, ...?

Even with the inconsistency it will introduce, I would take this opportunity to use more descriptive naming for the embedded APIs, rather than forward propagating usage of abbreviations.

  1. Anecdotally, it is noticeably harder to parse this naming scheme in the documentation.
  2. Abbreviated names will reduce readability in implementation code.
  3. Specifically, ect is so visually similar to ec that it is begging for transposition typos 🙂.

"name": "type",
"description": "The type of authorization business is requesting from the host.",
"type": "string",
"enum": ["api_key", "oauth"]
Copy link

@gsmith85 gsmith85 Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An example of this: 040684c

This is a reminder to add a blurb about avoiding enums, for forward compatibility in schema-authoring.md, I can create a PR.

{
"name": "ect.ready",
"summary": "Handshake from business to host",
"description": "Initiates the Embedded Cart Protocol. Business declares which delegation it supports, host responds with optional channel upgrade and initial cart state.",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current initialization flow requires two round trips for initialization. Is there a reason we can't support proactive auth and reduce it to one?

If the business includes a requested_auth type the Host can optionally return the token in the readyResult. This reduces UI flicker and bridge latency, especially on mobile where native-to-web serialization is expensive.

If we want to maintain symmetry this could be added to ec.ready as well?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

TC review Ready for TC review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants