Skip to content
Merged
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
140 changes: 122 additions & 18 deletions docs/specification/embedded-checkout.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ indicate ECP availability and allowed delegations for a specific session.
"version": "{{ ucp_version }}",
"transport": "embedded",
"config": {
"delegate": ["payment.credential", "fulfillment.address_change"]
"delegate": ["payment.credential", "fulfillment.address_change", "window.open"]
}
}
]
Expand Down Expand Up @@ -250,12 +250,13 @@ message following a consistent pattern: `ec.{delegation}_request`
| `payment.instruments_change` | `ec.payment.instruments_change_request` |
| `payment.credential` | `ec.payment.credential_request` |
| `fulfillment.address_change` | `ec.fulfillment.address_change_request` |
| `window.open` | `ec.window.open_request` |

Extensions define their own delegation identifiers; see each extension's
specification for available options.

```text
?ec_version=2026-01-11&ec_delegate=payment.instruments_change,payment.credential,fulfillment.address_change
?ec_version=2026-01-11&ec_delegate=payment.instruments_change,payment.credential,fulfillment.address_change,window.open
```

#### Color Scheme
Expand Down Expand Up @@ -333,7 +334,7 @@ The Embedded Checkout determines which delegations to honor based on:
The Embedded Checkout **MUST** indicate accepted delegations in the `ec.ready`
request via the `delegate` field (see [`ec.ready`](#ecready)). If a
requested delegation is not accepted, the Embedded Checkout **MUST** handle that
capability using its own UI.
action using its own UI.

#### Binding Requirements

Expand All @@ -354,17 +355,18 @@ capability using its own UI.

#### 3.3.3 Delegation Flow

1. **Request**: Embedded Checkout sends an `ec.{capability}.{action}_request`
1. **Request**: Embedded Checkout sends an `ec.{domain}.{action}_request`
message with current state (includes `id`)
2. **Native UI**: Host presents native UI for the delegated action
3. **Response**: host sends back a JSON-RPC response with matching `id` and
`result` or `error`
4. **Update**: Embedded Checkout updates its state and may send subsequent
change notifications

See [Payment Extension](#payment-extension) and
[Fulfillment Extension](#fulfillment-extension) for
capability-specific delegation details.
See [Payment Extension](#payment-extension),
[Fulfillment Extension](#fulfillment-extension), and
[Window Extension](#window-extension) for
domain-specific delegation details.

### Navigation Constraints

Expand Down Expand Up @@ -501,14 +503,15 @@ all implementations. All messages are sent from Embedded Checkout to host.
Extensions **MAY** extend the Embedded protocol by defining additional messages.
Extension messages **MUST** follow the naming convention:

- **Notifications**: `ec.{capability}.change` — state change notifications (no
- **Notifications**: `ec.{domain}.change` — state change notifications (no
`id`)
- **Delegation requests**: `ec.{capability}.{action}_request` — requires
- **Delegation requests**: `ec.{domain}.{action}_request` — requires
response (has `id`)

Where:

- `{capability}` matches the capability identifier from discovery
- `{domain}` matches the domain identifier from discovery (e.g., `payment`,
`fulfillment`, `window`)
- `{action}` describes the specific action being delegated (e.g.,
`instruments_change`, `address_change`)
- `_request` suffix signals this is a delegation point requiring a response
Expand Down Expand Up @@ -554,7 +557,7 @@ actions.
"id": "ready_1",
"method": "ec.ready",
"params": {
"delegate": ["payment.credential", "fulfillment.address_change"]
"delegate": ["payment.credential", "fulfillment.address_change", "window.open"]
}
}
```
Expand Down Expand Up @@ -1269,6 +1272,106 @@ The address object uses the UCP

{{ schema_fields('postal_address', 'embedded-checkout') }}

## Window Extension

The window extension defines how the Embedded Checkout notifies the host when
the buyer activates a link presented by the business. When a checkout URL
includes `ec_delegate=window.open`, the host **MUST** handle every
`ec.window.open_request` and acknowledge the request.

This is distinct from
[Navigation Constraints](#navigation-constraints), which the Embedded Checkout
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you for calling out this similar-but-different concept 👍

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

+1

enforces unconditionally to prevent navigation to unrelated pages.

### Window Overview & Host Choice

Window delegation allows for two different patterns:

**Option A: Host Delegates to Embedded Checkout** The host does NOT include
`window.open` in `ec_delegate`. The Embedded Checkout handles link presentation
using its own inline UI. This is the standard, non-delegated flow.

**Option B: Host Takes Control** The host includes
`ec_delegate=window.open` in the Checkout URL, informing the Embedded Checkout
to send `ec.window.open_request` when the buyer activates a link. When delegated:

**Embedded Checkout responsibilities**:

- **MUST** send `ec.window.open_request` when the buyer activates a link
presented by the business

**Host responsibilities**:

- **MUST** validate that the requested URL uses the `https` scheme
- **SHOULD** apply additional host security policies (e.g., verifying
origins)
- **MUST** present the content to the buyer for every approved request
(e.g., in a modal, new tab, or similar)
- **MUST** respond with a JSON-RPC success result when the request was
processed, or a `window_open_rejected_error` error if host policy prevented
the navigation
- **MAY** notify the buyer if the request was rejected

By accepting `window.open` delegation, the host assumes responsibility for
handling the buyer's link interactions. The Embedded Checkout **MUST NOT**
present its own UI for the link.

The `ec.window.open_request` payload contains only the URL. Hosts that need
richer context (e.g., link type or label) **MAY** cross-reference the requested
URL against the `checkout.links` array from the checkout session to obtain
additional metadata.

### Window Message API Reference

#### `ec.window.open_request`

Requests the host to handle a link activated by the buyer within the checkout.

- **Direction:** Embedded Checkout → Host
- **Type:** Request
- **Payload:**
- `url` (string, uri, **REQUIRED**): The URL of the resource to present.

**Example Message:**

```json
{
"jsonrpc": "2.0",
"id": "window_1",
"method": "ec.window.open_request",
"params": {
"url": "https://merchant.com/privacy-policy"
}
}
```

- **Direction:** Host → Embedded Checkout
- **Type:** Response
- **Payload:** Empty object (`{}`).

**Example Success Response:**

```json
{
"jsonrpc": "2.0",
"id": "window_1",
"result": {}
}
```

**Example Error Response:**

```json
{
"jsonrpc": "2.0",
"id": "window_1",
"error": {
"code": "window_open_rejected_error",
"message": "Window open rejected by host."
}
}
```

## Security & Error Handling

### Error Codes
Expand All @@ -1279,13 +1382,14 @@ error codes mapped to
**[W3C DOMException](https://webidl.spec.whatwg.org/#idl-DOMException)** names
where possible.

| Code | Description |
| :-------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- |
| `abort_error` | The user cancelled the interaction (e.g., closed the sheet). |
| `security_error` | The host origin validation failed. |
| `not_supported_error` | The requested payment method is not supported by the host. |
| `invalid_state_error` | Handshake was attempted out of order. |
| `not_allowed_error` | The request was missing valid User Activation (see [Prevention of Unsolicited Payment Requests](#prevention-of-unsolicited-payment-requests)). |
| Code | Description |
| :--------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- |
| `abort_error` | The user cancelled the interaction (e.g., closed the sheet). |
| `security_error` | The host origin validation failed. |
| `not_supported_error` | The requested payment method is not supported by the host. |
| `invalid_state_error` | Handshake was attempted out of order. |
| `not_allowed_error` | The request was missing valid User Activation (see [Prevention of Unsolicited Payment Requests](#prevention-of-unsolicited-payment-requests)). |
| `window_open_rejected_error` | Host policy prevented the navigation. The host **MAY** notify the buyer that their request was rejected. |

### Security for Web-Based Hosts

Expand Down
26 changes: 25 additions & 1 deletion source/services/shopping/embedded.openrpc.json
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,31 @@
}
}
}
},

{
"name": "ec.window.open_request",
"summary": "Request window open",
"description": "The buyer activated a link within checkout. The host MUST present the content to the buyer and respond with a success result, or respond with a window_open_rejected_error error if host policy prevented the navigation.",
"params": [
{
"name": "url",
"required": true,
"schema": {
"type": "string",
"format": "uri",
"description": "The URL of the resource to present."
}
}
],
"result": {
"name": "windowOpenResult",
"schema": {
"type": "object",
"description": "Acknowledgement that the host handled the request."
}
}
}
],
"x-delegations": ["payment.instruments_change", "payment.credential"]
"x-delegations": ["payment.instruments_change", "payment.credential", "window.open"]
}
Loading