Embedded Checkout
Drop-in hosted checkout iframe for JavaScript.
Embedded Checkout renders Easy Labs' hosted checkout page inside an iframe on your own domain. Card collection, validation, 3DS, and payment confirmation all happen inside the iframe — your page never touches the PAN, so you stay out of PCI scope. The iframe is loaded from https://checkout.itseasy.co/embed and authenticates against a short-lived client_secret.
Use it when you want a payment surface that just works and is themed by Easy Labs. If you need to fully control the UI of the form (custom card layout, custom buttons), you'll need a different integration path — embedded checkout is intentionally opinionated.
Server: create a checkout session
Mint the session on your server using the secret API key. The response includes a client_secret that you forward to the browser:
// server.ts
import { createClient } from "@easylabs/node";
const easy = await createClient({ apiKey: process.env.EASY_API_KEY! });
const { data } = await easy.createEmbeddedCheckoutSession({
mode: "payment", // or "subscription"
line_items: [{ price_id: "price_...", quantity: 1 }],
customer_email: "buyer@example.com",
success_url: "https://example.com/success",
cancel_url: "https://example.com/cancel",
payment_methods: ["card", "crypto"],
metadata: { order_id: "ord_42" },
});
// data.client_secret — forward this to the browser.
// data.id, data.amount_total, data.currency, data.expires_at also available.The same createEmbeddedCheckoutSession method exists on the browser client, but never call it from the browser — it requires your secret API key. Always create sessions server-side.
You also need to whitelist the parent origin (the domain hosting the iframe) in your embedded-checkout config:
await easy.updateEmbeddedCheckoutConfig({
allowed_origins: ["https://checkout.acmecorp.com", "https://*.acmecorp.com"],
});An empty allowed_origins array disables embedding entirely.
Client: mount the iframe
import { mountEmbeddedCheckout } from "@easylabs/browser";
const handle = mountEmbeddedCheckout("#checkout", {
clientSecret,
onReady: () => console.log("ready"),
onCryptoConfirmed: (event) => console.log("crypto confirmed", event),
});mountEmbeddedCheckout(target, options) accepts:
| Argument | Type | Description |
|---|---|---|
target | Element | string | A DOM element or a CSS selector. If a selector matches no element, the function throws synchronously. |
Options
| Option | Type | Default | Description |
|---|---|---|---|
clientSecret | string | required | Returned by createEmbeddedCheckoutSession. Never use the merchant API key here. |
minHeight | string | "500px" | Minimum iframe height before the checkout reports its actual size. |
style | Partial<CSSStyleDeclaration> | {} | Inline style overrides applied to the iframe. Defaults (width: 100%, border: none, colorScheme: normal) are applied first, so any property you set wins. |
className | string | — | className applied to the iframe element. |
onReady | () => void | — | Fired when the iframe completes its initial postMessage handshake. |
onCryptoConfirmed | (data: unknown) => void | — | Fired when the checkout reports a crypto payment as confirmed. Forwarded as-received; see the checkout.session.crypto_confirmed webhook for the canonical schema. |
__checkoutUrl | string | https://checkout.itseasy.co | Internal. Override the checkout origin for development. Public consumers should leave this unset. |
Return value: EmbeddedCheckoutHandle
interface EmbeddedCheckoutHandle {
/** Re-initialize the iframe with a new client_secret. */
update(clientSecret: string): void;
/** Remove the iframe and stop listening for postMessage events. */
unmount(): void;
/** The underlying <iframe> element, exposed for advanced layout. */
readonly iframe: HTMLIFrameElement;
}update() is safe to call before the iframe has signalled ready — the new secret will be sent as soon as the handshake completes.
Events
Communication between your page and the iframe runs over postMessage. The SDK only accepts messages whose event.origin matches the resolved checkout origin and whose event.source is the iframe's contentWindow. Three message types are recognised:
Message type | Direction | Effect |
|---|---|---|
easylabs:ready | iframe → parent | Marks the iframe as initialized. Triggers onReady and the initial easylabs:init send (or re-send if clientSecret changed via update). |
easylabs:resize | iframe → parent | Sets iframe.style.height = "<height>px". Lets the iframe grow with its content. |
easylabs:crypto_confirmed | iframe → parent | Triggers onCryptoConfirmed(event.data). |
easylabs:init | parent → iframe | Sent automatically on ready and on update. Carries { clientSecret }. |
Card-payment success is communicated via the iframe redirecting itself to your success_url — there's no onCardConfirmed event because the iframe leaves the page on success.
For server-side reactions (fulfillment, receipts, accounting), listen for the checkout.session.completed webhook. Don't trust client events as the source of truth.
Cleanup
mountEmbeddedCheckout registers a message listener on window and inserts an <iframe> into the DOM. Call handle.unmount() when navigating away to remove both:
const handle = mountEmbeddedCheckout("#checkout", { clientSecret });
// later — e.g. on route change in your SPA:
handle.unmount();Forgetting to unmount leaks one listener per mount. In a long-lived SPA that mounts checkout repeatedly, this matters.
Customization
Customization is intentionally narrow. You control:
- Layout — the size and position of the iframe in your page (via
style,className,minHeight, or by readinghandle.iframe). - Branding inside the iframe —
merchant.name,merchant.logo, andmerchant.primary_colorare configured per-merchant in the dashboard and applied automatically. - Payment methods —
payment_methods: ["card", "crypto"]on the session. - Locale / currency — driven by the session's
currencyand the buyer's browser.
You cannot restyle the form fields themselves, change the field order, or swap the submit button — those are part of the hosted experience. If you need that level of control, talk to us about alternative integration paths.