Easy Labs
SDKsReact

Embedded Checkout

Drop-in hosted checkout iframe for React.

<EmbeddedCheckout> mounts an Easy Labs–hosted checkout flow inside an iframe in your page. Use it when you want a complete checkout — including card capture, ACH, and crypto payments — without building the form yourself, but still want it embedded in your own UI rather than redirecting the customer away.

The iframe is loaded from https://checkout.itseasy.co/embed, auto-resizes to its content, and communicates with your page through a small postMessage protocol. Wrap it in <EmbeddedCheckoutProvider> to get success / error / close callbacks and a useEmbeddedCheckout() status hook.

Server: create a checkout session

Call createEmbeddedCheckoutSession from your server (or the client useEasy() hook for prototypes) to get a client_secret. This must happen server-side in production so your API key stays out of the browser.

app/checkout/session.ts (Node SDK)
import { Easy } from "@easylabs/node";

const easy = new Easy(process.env.EASY_API_KEY!);

export async function createSession() {
  const res = await easy.createEmbeddedCheckoutSession({
    line_items: [{ price_id: "price_123", quantity: 1 }],
    mode: "payment",
    success_url: "https://example.com/checkout/success",
    cancel_url: "https://example.com/checkout/cancel",
    payment_methods: ["card", "crypto"],
  });
  return res.data.client_secret;
}

For the matching backend reference, see Node SDK → Embedded Checkout.

Client: mount the iframe

Pass the client_secret to <EmbeddedCheckout>. Wrap it in <EmbeddedCheckoutProvider> if you want lifecycle callbacks.

src/app/checkout/page.tsx
"use client";

import {
  EmbeddedCheckout,
  EmbeddedCheckoutProvider,
  useEmbeddedCheckout,
} from "@easylabs/react";
import { useEffect, useState } from "react";

function Status() {
  const { status } = useEmbeddedCheckout();
  return <p>Checkout status: {status}</p>;
}

export default function CheckoutPage() {
  const [clientSecret, setClientSecret] = useState<string>();

  useEffect(() => {
    fetch("/api/checkout/session", { method: "POST" })
      .then((r) => r.json())
      .then((d) => setClientSecret(d.clientSecret));
  }, []);

  if (!clientSecret) return <p>Loading…</p>;

  return (
    <EmbeddedCheckoutProvider
      config={{
        clientSecret,
        onSuccess: ({ sessionId, status, tx_signature }) => {
          console.log("paid", { sessionId, status, tx_signature });
          window.location.href = "/checkout/success";
        },
        onError: (err) => console.error(err),
        onClose: () => console.log("closed"),
      }}
    >
      <Status />
      <EmbeddedCheckout clientSecret={clientSecret} />
    </EmbeddedCheckoutProvider>
  );
}

<EmbeddedCheckout> and <EmbeddedCheckoutProvider> are both client components ("use client"). They do not depend on EasyProvider — the iframe is self-contained — so you can render them anywhere.

Props

<EmbeddedCheckout>

PropTypeRequiredDescription
clientSecretstringyesThe client_secret returned by createEmbeddedCheckoutSession. Updating this prop re-sends the easylabs:init postMessage to the existing iframe (the iframe src is not reset). To force a full iframe reload — for example, after a session expires — remount the component, e.g. by changing its React key prop.
classNamestringnoClass on the wrapping <div>.
styleCSSPropertiesnoStyle overrides merged onto the iframe (width: 100%, minHeight: 500px, no border by default).
__checkoutUrlstringnoInternal. Override the checkout origin for development.

<EmbeddedCheckoutProvider>

Takes a single config prop:

FieldTypeDescription
clientSecretstringSame value passed to <EmbeddedCheckout>. Used to authorize incoming events.
onSuccess(data: { sessionId: string; status: string; tx_signature?: string | null }) => voidFires once for card success and once for confirmed crypto payments. tx_signature is only present on crypto.
onError(error: string) => voidFires if the hosted checkout reports an error.
onClose() => voidFires when the customer closes the embedded experience.
__checkoutUrlstringInternal. Override the checkout origin for development.

useEmbeddedCheckout()

Inside an <EmbeddedCheckoutProvider>, useEmbeddedCheckout() returns:

{ status: "loading" | "ready" | "complete" | "error" }
  • "loading" — the iframe has not yet sent its easylabs:ready handshake.
  • "ready" — the iframe is rendered and accepting input.
  • "complete"easylabs:success or easylabs:crypto_confirmed arrived.
  • "error"easylabs:error arrived; check onError.

Events: the postMessage protocol

The iframe and your page exchange JSON postMessage events. <EmbeddedCheckout> and <EmbeddedCheckoutProvider> handle all of these for you — you only need to know about them if you're debugging or building a custom integration.

All messages have a type field prefixed with easylabs:. Both sides validate the event.origin against the checkout URL, and the provider additionally requires that callback-firing events carry a payload whose client_secret (or clientSecret / checkout_client_secret) matches the active session — origin alone isn't enough since other windows on the same origin could otherwise spoof success.

DirectionTypePayloadPurpose
iframe → parenteasylabs:readyIframe finished mounting; parent responds with easylabs:init.
parent → iframeeasylabs:init{ clientSecret }Hand the session secret to the iframe. Re-sent if clientSecret prop changes.
iframe → parenteasylabs:resize{ height: number }Auto-resize the iframe to its content. Handled automatically.
iframe → parenteasylabs:success{ sessionId, status, ... } (with matching client_secret)Card / standard payment succeeded. Triggers onSuccess and status: "complete".
iframe → parenteasylabs:crypto_confirmed{ session_id, tx_signature, client_secret }On-chain crypto payment confirmed. Triggers onSuccess and status: "complete".
iframe → parenteasylabs:close{ client_secret }Customer closed the checkout. Triggers onClose.
iframe → parenteasylabs:error{ error: string, client_secret }Checkout reported an error. Triggers onError and status: "error".

If you're polling crypto status server-side, use useEasy().getCryptoPaymentStatus(sessionId) as a fallback to the easylabs:crypto_confirmed event.

Customization

The hosted page handles the entire payment UI — fields, validation, error states, and the on-chain UX for crypto payments. What you can control today:

  • Container layout. Use className and style on <EmbeddedCheckout> to set max width, border, shadow, and so on. The iframe defaults to width: 100% and minHeight: 500px.
  • Payment methods. Pass payment_methods: ["card"], ["crypto"], or both when creating the session.
  • Return / success / cancel URLs. Pass return_url, success_url, and cancel_url to createEmbeddedCheckoutSession. The hosted page also fires easylabs:success / easylabs:close so you can navigate from your callbacks instead.
  • Customer pre-fill. Pass customer_email to createEmbeddedCheckoutSession.

On this page