The Embedded checkout integration enables your web-based checkout to be
embedded in Google surfaces. Use this path if your product requires complex
logic (e.g., customizations) that the Native API cannot support. You will
implement a checkout UI that will be embedded in the checkout flow through an
iframe.

## What is embedded checkout?

Embedded checkout (EC) allows a host (like Google Search or an AI Agent) to
display your existing web-based checkout within their application (using an
iframe or webview). Unlike a standard web redirect, this allows for
bi-directional communication. The host can "delegate" specific tasks such as
selecting a saved address or paying with a stored credential to provide a
faster, native-feeling experience, while you remain the Merchant of Record and
handle the actual order creation.

## Merchant implementation checklist

To support Embedded Checkout, you must implement the following requirements
across your UCP API and your frontend checkout application.

### 1. Enable discovery (server)

You must declare that your services support the embedded protocol in your UCP
profile.

- **Action** : Add the embedded object to your `/.well-known/ucp` service definition.
- **Requirement**: Include the OpenRPC schema URL.
- **Version**: Ensure the version matches your UCP service version.

    "services": {
      "dev.ucp.shopping": {
        "version": "2026-01-11",
        "spec": "https://ucp.dev/specification/overview",
        "embedded": {
          "schema": "https://ucp.dev/services/shopping/embedded.openrpc.json"
        }
      }
    }

### 2. Handle URL initialization (frontend)

When the host loads your `continue_url`, they will append specific query
parameters. Your frontend must parse these immediately upon loading.

- **Action** : Parse the following URL query parameters:
  - `ec_version`: The protocol version (e.g., `2026-01-11`).
  - `ec_auth`: (Optional) A business-defined token for session validation.
  - `ec_delegate`: A comma-separated list of actions the host wants to handle natively (e.g., `payment.credential`, `fulfillment.address_change`, `payment.instruments_change`).

### 3. Establish communication (frontend)

Communication uses JSON-RPC 2.0 using `postMessage`.

- **Action**: Implement a listener for message events.
- **Requirement** : You must validate the `origin` of every message to ensure it matches the host.
- **Native support** : If `postMessage` is unavailable in a native app context, utilize `window.EmbeddedCheckoutProtocolConsumer` or `window.webkit.messageHandlers.EmbeddedCheckoutProtocolConsumer`.

### 4. Perform the handshake (frontend)

Send a request to the host to confirm readiness and accepted delegations. This
is a request that requires a response.

- **Action** : Send the `ec.ready` request with an `id`.
- **The "Two Handshakes" Rule** : You may need to perform this handshake twice.
  - First Handshake: Sent using standard window.postMessage. This is used to "check-in" with the host.
  - Channel Upgrade: The host may respond with an upgrade object containing a MessagePort. This creates a private, dedicated communication channel between you and the host.
  - Second Handshake: If an upgrade is provided, you must send a new `ec.ready` request over the new port to confirm the secure channel is active.
  - Data Retrieval: The host responds to this second request with the `result.checkout` object. This is where you finally receive the payment instruments to populate your UI.
- **Payload**: Include a delegate array listing the capabilities you accept.

    // Example: Handshake with Port Upgrade logic
    const EXPECTED_HOST_ORIGIN = "https://www.google.com";
    const hostWindow = window.parent;

    function sendHandshake(target, id = "ready_1") {
      const message = JSON.stringify({
        "jsonrpc": "2.0",
        "id": id,
        "method": "ec.ready",
        "params": {
          "delegate": ["payment.credential", "fulfillment.address_change"]
        }
      });

      if (target instanceof MessagePort) {
        target.postMessage(message);
      } else {
        // Explicitly set the targetOrigin for window communication
        target.postMessage(message, EXPECTED_HOST_ORIGIN);
      }
    }

    sendHandshake(hostWindow);

    window.addEventListener("message", (event) => {
      // Enforce that the message came strictly from the embedding parent window
      if (event.source !== window.parent) return;

      // Verify the origin matches the expected host environment
      if (event.origin !== EXPECTED_HOST_ORIGIN) return;

      // Safely parse the payload
      let response;
      try {
        response = JSON.parse(event.data);
      } catch (e) {
        return;
      }

      if (response.id === "ready_1" && response.result?.upgrade?.port) {
        const privatePort = response.result.upgrade.port;
        privatePort.start();
        // Send second handshake over the MessagePort with a unique ID
        sendHandshake(privatePort, "ready_2");
      }
    });

### 5. Implement delegation logic (frontend)

When a delegation is accepted, you must hide your internal UI and request data
from the host when needed.

- **Action**: Hide the relevant UI elements for delegated tasks.
- **Requirement**: Update your checkout state using a PUT-style replacement (replace the whole object section) based on the data returned by the host. Even if a field is delegated, you must still include it in the checkout object for every notification to ensure the host has a "single source of truth" for the entire state of the checkout.

    // Example: Requesting a payment credential (token) from the host
    communicationChannel.postMessage(JSON.stringify({
      "jsonrpc": "2.0",
      "id": "req_token_1",
      "method": "ec.payment.credential_request",
      "params": {
          "checkout": ... // pass the credential data that needs to be updated
      }
    }));

    communicationChannel.onmessage = (event) => {
      const msg = JSON.parse(event.data);
      if (msg.id === "req_token_1" && msg.result) {
          localCheckout.payment.credentials = msg.result.checkout.payment.credentials;
          renderUI();
      }
    };

### 6. Send lifecycle \& state updates (frontend)

Inform the host of changes within the iframe using notifications (no id).

- **Action** : Send notifications when state changes:
  - `ec.start`: When the checkout is fully visible.
  - `ec.line_items.change`: Cart contents (quantity changed, items added/removed) updated in the checkout UI.
  - `ec.buyer.change`: Buyer contact or identification info updated.
  - `ec.payment.change`: Payment details changed within the embedded UI.
  - `ec.fulfillment.change`: Fulfillment details (shipping, method) changed within the embedded UI.
  - `ec.complete`: When the order is successfully placed.
  - `ec.messages.change`: Checkout messages have been updated. Messages include errors, warnings, and informational notices about the checkout state.

### 7. Enforce security (server/headers)

You must ensure your checkout cannot be embedded by malicious actors.

- **Action**: Implement Content Security Policy (CSP) headers.
- **Requirement** : Set `frame-ancestors <host_origin>;` to allow embedding only by trusted hosts. Every `postMessage` received must have its `origin` strictly validated against the expected host before processing.
- **Navigation**: Block logic that navigates the user away from the checkout flow (e.g., remove "Continue Shopping" links that lead to your homepage). Exceptions are allowed for 3DS verification or 3rd party payment redirects.
- **Iframe Context** : Your application must be compatible with the `sandbox` attribute (`allow-scripts allow-forms allow-same-origin`) and the `credentialless` attribute, which creates an ephemeral, private session.
- **Prohibition**: You must never trigger a payment token request programmatically; the host will only release a token if there is a valid User Activation.