Skip to main content

Embedded Hosted Checkout - COPE payment directly in your page

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.

📄 Full documentation: [Embedded hosted checkout - COPE]


Requirements

Before creating embedded checkout sessions:

  • Create a publishable key for the COPE business

  • Register every parent page origin that may host the iFrame (e.g. https://shop.example.com)

  • Use an origin only: scheme, host, and optional port – no path, query string, or fragment

  • HTTPS only in production (http://localhost for development only)

  • 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.


Integration

ts

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

Redirect route, e.g. https://app.cope.com/checkout/:token

checkout.embedCheckoutUrl

iFrame route, e.g. https://app.cope.com/checkout/embed/:token

checkout.embedOrigin

Approved parent origin returned by COPE

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:

  • srccheckout.embedCheckoutUrl

  • titleCOPE checkout

  • allowpayment * for browser wallet support

  • referrerPolicyno-referrer

  • width100%

  • min-height720px

  • No sandbox attribute

Do not build the iFrame manually – the SDK also performs the trusted mount handshake and filters all iFrame messages by origin, source window, message source, and version.


Fallback

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

ts

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

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.

Event

Meaning

onReady

iFrame loaded and ready

onSuccess

Payment completed successfully

onCancel

User cancelled

onError

Error in payment process (incl. retryable flag)

onResize

iFrame height changed

For the raw sanitized event stream:

ts

cope.mountCheckout("#cope-checkout-frame", checkout, {   onMessage: (event) => {     analytics.track("cope_checkout_embed_event", event)   }, })

Security model

Embedded checkout has two layers of authorization:

  1. COPE validates embed_origin against the business allowlist when the checkout session is created

  2. The iFrame route validates the same embed contract before rendering the payment form

The SDK sends the mount message with a specific targetOrigin – never *. iFrame messages are only accepted when event.origin, event.source, event.data.source (cope.checkout), and event.data.version (1) all match.


Use cases

In-page checkout modal
The payment form opens as an overlay within your page. No redirect, no loss of context – the user stays within your navigation and layout throughout the entire checkout.

SaaS upgrade flow
A user upgrades from free to pro directly in the dashboard. mountCheckout() renders the payment form as an iFrame in a modal or dedicated section of the page, without losing the current state.

Custom storefront
You build a fully custom product page with your own branding – COPE only renders the payment form inside the iFrame. Your page retains full control over layout, colors, and user experience.

High-conversion checkout
Every redirect is a potential drop-off point. Embedded checkout eliminates that friction and keeps the user on your page all the way to successful payment.


Troubleshooting

Error

Likely cause

Fix

embed_origin_not_allowed

Origin not registered

Register the exact parent origin, create a new session

mountCheckout requires checkout.embedOrigin

checkout() called without embed_origin

Pass embed_origin: window.location.origin

Checkout embed origin ... does not match

Session created for a different origin

Call checkout() and mountCheckout() from the same origin

iFrame never becomes ready

Token expired, blocked, or handshake failed

Use fallback: "redirect", inspect onError and browser console

Browser wallet unavailable

Wallet domain registration incomplete

Verify allow="payment *" and wallet setup for both origins

Did this answer your question?