> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cope.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Embedded hosted checkout

> Mount COPE hosted checkout in an iframe with the Checkout SDK, registered embed origins, and trusted postMessage events.

# Embedded hosted checkout

Embedded hosted checkout lets your page keep its layout while COPE renders the payment form inside an iframe. Your site owns the surrounding experience. COPE owns the checkout route, payment collection, tax finality, order creation, and buyer-facing terminal states.

Use embedded checkout when you need a custom storefront or in-page checkout modal. Use redirect checkout when the simplest integration is enough or when a browser blocks the iframe flow.

## Requirements

Before creating embedded checkout sessions:

* Create a Checkout SDK publishable key for the COPE business.
* Register every parent page origin that may host the iframe, for example `https://shop.example.com`.
* Use an origin only: scheme, host, and optional port. Do not include a path, query string, fragment, or userinfo.
* Use HTTPS in production. `http://localhost` is only for development and test environments.
* Register the success and cancel URLs used for redirect-based completion or fallback. They are matched as complete URL strings, not as origins — see [redirect URLs](./overview#redirect-urls).

The parent origin must exactly match the browser page origin that calls `checkout({ embed_origin })`. `https://shop.example.com` and `https://www.shop.example.com` are different origins.

Embed origins and redirect URLs are separate allowlists with different matching rules. An embed origin is scheme, host, and optional port only. A redirect URL is the entire URL, including its path and any query string.

## Create an iframe checkout

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { CopeCart } from "@copecart/sdk"

const cope = new CopeCart({
  publishableKey: "cope_pk_live_...",
})

async function openEmbeddedCheckout(productId: string, planId: number) {
  const cart = await cope.createCart({ currency: "EUR" })

  await cope.addLine(cart.id, {
    product_id: productId,
    plan_id: planId,
  })

  await cope.setBuyerIdentity(cart.id, {
    email: "buyer@example.com",
    tax_location: {
      country: "DE",
      postal_code: "10115",
    },
  })

  await cope.reprice(cart.id)

  const checkout = await cope.checkout(cart.id, {
    embed_origin: window.location.origin,
    success_url: "https://shop.example.com/thank-you",
    cancel_url: "https://shop.example.com/cart",
    consents: [{ type: "buyer_tos" }],
  })

  return cope.mountCheckout("#cope-checkout-frame", checkout, {
    fallback: "redirect",
    onReady: () => showCheckoutFrame(),
    onResize: ({ height }) => resizeContainer(height),
    onSuccess: () => showOrderConfirmation(),
    onCancel: () => closeCheckoutFrame(),
    onError: ({ code, retryable }) => reportCheckoutError(code, retryable),
  })
}
```

The checkout response includes both URL shapes:

| Field                       | Meaning                                                                             |
| --------------------------- | ----------------------------------------------------------------------------------- |
| `checkout.checkoutUrl`      | Hosted checkout redirect route, for example `https://app.cope.com/checkout/:token`. |
| `checkout.embedCheckoutUrl` | Iframe route, for example `https://app.cope.com/checkout/embed/:token`.             |
| `checkout.embedOrigin`      | The approved parent origin returned by COPE when `embed_origin` is valid.           |

`mountCheckout()` refuses to mount when `checkout.embedOrigin` is missing or does not exactly match `window.location.origin`.

## Mount behavior

`mountCheckout()` creates the iframe for you:

* `src` is `checkout.embedCheckoutUrl`.
* `title` defaults to `COPE checkout`.
* `allow` is set to `payment *` for browser wallet support.
* `referrerPolicy` is set to `no-referrer`.
* `width` is set to `100%`.
* `min-height` is set to `720px`.
* No `sandbox` attribute is added by the SDK.

Do not build the iframe manually unless you are testing the embed contract. The SDK also performs the trusted mount handshake and filters all iframe messages by origin, source window, message source, and version.

## Fallbacks

Set `fallback: "redirect"` for buyer-safe recovery if the iframe does not complete the trusted ready handshake before `readyTimeoutMs`.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
cope.mountCheckout("#cope-checkout-frame", checkout, {
  fallback: "redirect",
  readyTimeoutMs: 8000,
  onFallbackRedirect: () => {
    console.log("Falling back to hosted checkout")
  },
})
```

With the default `fallback: "error"`, the SDK calls:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
onError({ code: "load_failed", retryable: true })
```

## Events

The iframe sends sanitized events to the SDK. Callback payloads do not include checkout credentials, payment client secrets, or buyer PII.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
type CheckoutEmbedEvent =
  | { type: "ready"; messageId?: string; timestamp?: string }
  | {
      type: "resize"
      payload: { height: number }
      messageId?: string
      timestamp?: string
    }
  | {
      type: "error"
      payload: {
        code: "not_found" | "load_failed" | "embed_not_allowed" | "confirm_failed"
        retryable: boolean
      }
      messageId?: string
      timestamp?: string
    }
  | {
      type: "terminal"
      payload: {
        status: "expired" | "completed" | "unavailable" | "cancelled" | "processing"
      }
      messageId?: string
      timestamp?: string
    }
```

Use `onMessage` when you need the raw sanitized event stream:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
cope.mountCheckout("#cope-checkout-frame", checkout, {
  onMessage: (event) => {
    analytics.track("cope_checkout_embed_event", event)
  },
})
```

`onSuccess` and `onCancel` are convenience callbacks derived from terminal events.

## Security model

Embedded checkout has two layers of authorization:

1. COPE validates `embed_origin` against the business checkout embed domain allowlist when checkout is created.
2. The iframe route validates the same embed contract before rendering checkout UI.

The SDK sends the mount message with a specific `targetOrigin`; it never uses `*`. It accepts iframe messages only when:

* `event.origin` matches the checkout iframe origin.
* `event.source` is the mounted iframe window.
* `event.data.source` is `cope.checkout`.
* `event.data.version` is `1`.

The regular hosted checkout route is frame-denied. Only `/checkout/embed/:token` is intended for iframe rendering.

## Test your integration

Use a development or staging page that matches the origin you registered for the COPE business. Exercise the full flow before launch:

1. Create a cart.
2. Add a product and payment plan.
3. Set buyer identity.
4. Reprice.
5. Create checkout with `embed_origin`.
6. Mount the iframe and inspect the postMessage conversation.

Also verify rejected unregistered origins, expired checkout sessions, missing mount handshakes, and redirect fallback behavior.

## Troubleshooting

| Symptom                                                   | Likely cause                                                                                                                 | Fix                                                                                                                                          |
| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `embed_origin_not_allowed` during checkout creation       | The page origin is not registered for the COPE business.                                                                     | Register the exact parent origin and create a new checkout session.                                                                          |
| `invalid_redirect_url` during checkout creation           | `success_url` or `cancel_url` is not registered. Registering the origin or the path without its query string does not match. | Register the complete URL exactly as sent, or omit the field to use the first registered URL. See [redirect URLs](./overview#redirect-urls). |
| `mountCheckout requires checkout.embedOrigin`             | Checkout was created without `embed_origin`, or the API rejected the embed origin.                                           | Pass `embed_origin: window.location.origin` and check the checkout response.                                                                 |
| `Checkout embed origin ... does not match current origin` | Checkout was created for a different parent origin.                                                                          | Create checkout from the same origin that will mount it.                                                                                     |
| Iframe never becomes ready                                | The token is invalid, expired, blocked by headers, or the mount handshake failed.                                            | Use `fallback: "redirect"` and inspect `onError` and browser console output.                                                                 |
| Browser wallet is unavailable                             | Wallet domain registration or cross-origin iframe payment permissions are incomplete.                                        | Confirm the iframe uses `allow="payment *"` and verify wallet domain setup for the parent and checkout origins.                              |
