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
107 changes: 102 additions & 5 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", "link.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` |
| `link.open` | `ec.link.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,link.open
```

#### Color Scheme
Expand Down Expand Up @@ -362,8 +363,9 @@ capability using its own UI.
4. **Update**: Embedded Checkout updates its state and may send subsequent
change notifications

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

### Navigation Constraints
Expand Down Expand Up @@ -554,7 +556,7 @@ actions.
"id": "ready_1",
"method": "ec.ready",
"params": {
"delegate": ["payment.credential", "fulfillment.address_change"]
"delegate": ["payment.credential", "fulfillment.address_change", "link.open"]
}
}
```
Expand Down Expand Up @@ -1269,6 +1271,100 @@ The address object uses the UCP

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

## Link Extension

The link 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=link.open`, the host **MUST** handle every
`ec.link.open_request` and acknowledge the request.

This is distinct from
[Navigation Constraints](#navigation-constraints), which the Embedded Checkout
enforces unconditionally to prevent navigation to unrelated pages.

### Link Overview & Host Choice

Link delegation allows for two different patterns:

**Option A: Host Delegates to Embedded Checkout** The host does NOT include
`link.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=link.open` in the Checkout URL, informing the Embedded Checkout
to send `ec.link.open_request` when the buyer activates a link. When delegated:

**Embedded Checkout responsibilities**:

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

**Host responsibilities**:

- **MUST** present the buyer with visible feedback for every
`ec.link.open_request` — either the content itself (e.g., in a modal,
side panel, or new tab) or a notification that their link request was
rejected

Choose a reason for hiding this comment

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

Do we want to mandate the Host to show an error to the Buyer for invalid links? I think that's more of an implementation choice. I'd suggest this

  • SHOULD validate the requested URL against 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 etc).
  • MUST respond with a JSON-RPC success (result) when the request was processed, or link_rejected if host policy prevented the navigation.
  • MAY notify the buyer if the request was rejected.

- **MUST** respond with a JSON-RPC success result when the link open
request was processed, or a `link_rejected` error if host policy
prevented the buyer's intent to open the link

By accepting `link.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 host has already provided the buyer
with visible feedback (content or rejection notification).

### Link Message API Reference

#### `ec.link.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": "link_1",
"method": "ec.link.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": "link_1",
"result": {}
}
```

**Example Error Response:**

```json
{
"jsonrpc": "2.0",
"id": "link_1",
"error": {
"code": "link_rejected",
"message": "Link rejected by host."
}
}
```

## Security & Error Handling

### Error Codes
Expand All @@ -1286,6 +1382,7 @@ where possible.
| `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)). |
| `link_rejected` | The host did not present the link content to the buyer. The host **MUST** notify the buyer that their link request was rejected. |

### Security for Web-Based Hosts

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

{
"name": "ec.link.open_request",
"summary": "Request link presentation",
"description": "The buyer activated a link within checkout. The host MUST present the content to the buyer and respond with a success result, or notify the buyer that the request was rejected and respond with a link_rejected error.",
"params": [
{
"name": "url",
"required": true,
"schema": {
"type": "string",
"format": "uri",
"description": "The URL of the resource to present."
}
}
],
"result": {
"name": "linkOpenResult",
"schema": {
"type": "object",
"description": "Acknowledgement that the host handled the link.",
"additionalProperties": false

Choose a reason for hiding this comment

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

Per #247 (comment), let's hold off on marking additionalProperties: false to maintain forward compatibility?

If we eventually want to use this pattern for Navigation Constraints/Permitted Exceptions, the host will need a way to pass data back to the iframe. Leaving the result object open allows for that future extensibility without breaking the core schema.

}
}
}
],
"x-delegations": ["payment.instruments_change", "payment.credential"]
"x-delegations": ["payment.instruments_change", "payment.credential", "link.open"]
}
Loading