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.
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.
"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>
| Prop | Type | Required | Description |
|---|---|---|---|
clientSecret | string | yes | The 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. |
className | string | no | Class on the wrapping <div>. |
style | CSSProperties | no | Style overrides merged onto the iframe (width: 100%, minHeight: 500px, no border by default). |
__checkoutUrl | string | no | Internal. Override the checkout origin for development. |
<EmbeddedCheckoutProvider>
Takes a single config prop:
| Field | Type | Description |
|---|---|---|
clientSecret | string | Same value passed to <EmbeddedCheckout>. Used to authorize incoming events. |
onSuccess | (data: { sessionId: string; status: string; tx_signature?: string | null }) => void | Fires once for card success and once for confirmed crypto payments. tx_signature is only present on crypto. |
onError | (error: string) => void | Fires if the hosted checkout reports an error. |
onClose | () => void | Fires when the customer closes the embedded experience. |
__checkoutUrl | string | Internal. Override the checkout origin for development. |
useEmbeddedCheckout()
Inside an <EmbeddedCheckoutProvider>, useEmbeddedCheckout() returns:
{ status: "loading" | "ready" | "complete" | "error" }"loading"— the iframe has not yet sent itseasylabs:readyhandshake."ready"— the iframe is rendered and accepting input."complete"—easylabs:successoreasylabs:crypto_confirmedarrived."error"—easylabs:errorarrived; checkonError.
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.
| Direction | Type | Payload | Purpose |
|---|---|---|---|
| iframe → parent | easylabs:ready | — | Iframe finished mounting; parent responds with easylabs:init. |
| parent → iframe | easylabs:init | { clientSecret } | Hand the session secret to the iframe. Re-sent if clientSecret prop changes. |
| iframe → parent | easylabs:resize | { height: number } | Auto-resize the iframe to its content. Handled automatically. |
| iframe → parent | easylabs:success | { sessionId, status, ... } (with matching client_secret) | Card / standard payment succeeded. Triggers onSuccess and status: "complete". |
| iframe → parent | easylabs:crypto_confirmed | { session_id, tx_signature, client_secret } | On-chain crypto payment confirmed. Triggers onSuccess and status: "complete". |
| iframe → parent | easylabs:close | { client_secret } | Customer closed the checkout. Triggers onClose. |
| iframe → parent | easylabs: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
classNameandstyleon<EmbeddedCheckout>to set max width, border, shadow, and so on. The iframe defaults towidth: 100%andminHeight: 500px. - Payment methods. Pass
payment_methods: ["card"],["crypto"], or both when creating the session. - Return / success / cancel URLs. Pass
return_url,success_url, andcancel_urltocreateEmbeddedCheckoutSession. The hosted page also fireseasylabs:success/easylabs:closeso you can navigate from your callbacks instead. - Customer pre-fill. Pass
customer_emailtocreateEmbeddedCheckoutSession.