Skip to main content

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.
  • Configure allowed success and cancel URLs for redirect-based completion or fallback.
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.

Create an iframe checkout

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:
FieldMeaning
checkout.checkoutUrlHosted checkout redirect route, for example https://app.cope.com/checkout/:token.
checkout.embedCheckoutUrlIframe route, for example https://app.cope.com/checkout/embed/:token.
checkout.embedOriginThe 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.
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:
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.
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:
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

SymptomLikely causeFix
embed_origin_not_allowed during checkout creationThe page origin is not registered for the COPE business.Register the exact parent origin and create a new checkout session.
mountCheckout requires checkout.embedOriginCheckout 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 originCheckout was created for a different parent origin.Create checkout from the same origin that will mount it.
Iframe never becomes readyThe 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 unavailableWallet 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.