Embed hosted checkout
Drop the Easy Labs hosted checkout iframe into your own page.
Goal
Render the Easy Labs checkout inside your own page so the buyer never leaves your site, while keeping all card / bank entry inside the hosted iframe (which means the merchant API key never reaches the browser, and your servers never touch raw PAN data). This is the recommended default for accepting first-time payments.
Prerequisites
- Easy Labs API key — see Quickstart.
@easylabs/nodefor the server,@easylabs/react(or@easylabs/browserfor vanilla JS) for the browser.- At least one published Product + Price you can charge for. Create them in the dashboard or with
client.createProduct/client.createPrice. - Your site's origin added to the merchant's
allowed_originsconfig. Set it once withclient.updateEmbeddedCheckoutConfig({ allowed_origins: ["https://your-app.com"] }).
Implementation
1. Create a session on the server
import { createClient } from "@easylabs/node";
const easy = await createClient({ apiKey: process.env.EASY_API_KEY! });
export async function POST() {
const { data: session } = await easy.createEmbeddedCheckoutSession({
mode: "payment",
line_items: [{ price_id: "price_01HXXXXXXXXXXX", quantity: 1 }],
success_url: "https://your-app.com/checkout/success",
cancel_url: "https://your-app.com/checkout/cancel",
customer_email: "ada@example.com",
payment_methods: ["card"],
});
return Response.json({ clientSecret: session.client_secret });
}session.client_secret authenticates the iframe against the session — it is safe to send to the browser. The merchant API key stays on the server.
2. Mount the iframe in the browser
With React:
"use client";
import { EmbeddedCheckout, EmbeddedCheckoutProvider } from "@easylabs/react";
import { useEffect, useState } from "react";
export function Checkout() {
const [clientSecret, setClientSecret] = useState<string | null>(null);
useEffect(() => {
fetch("/api/checkout-session", { method: "POST" })
.then((r) => r.json())
.then(({ clientSecret }) => setClientSecret(clientSecret));
}, []);
if (!clientSecret) return <p>Loading checkout…</p>;
return (
<EmbeddedCheckoutProvider
config={{
clientSecret,
onSuccess: ({ sessionId }) => {
window.location.href = `/checkout/success?session=${sessionId}`;
},
onError: (err) => console.error("checkout error", err),
onClose: () => console.log("buyer closed checkout"),
}}
>
<EmbeddedCheckout clientSecret={clientSecret} />
</EmbeddedCheckoutProvider>
);
}With vanilla JS / @easylabs/browser:
import { mountEmbeddedCheckout } from "@easylabs/browser";
const handle = mountEmbeddedCheckout("#checkout", {
clientSecret,
onReady: () => console.log("checkout ready"),
});
// later — destroy when navigating away:
handle.unmount();3. Confirm on the server (recommended)
Don't trust the browser's onSuccess alone — confirm the session server-side before fulfilling, either by handling the checkout.session.completed webhook or by reading the session status:
import { EasyWebhooks } from "@easylabs/node";
app.post("/webhooks/easy", async (req, res) => {
const event = EasyWebhooks.constructEvent(
req.rawBody,
req.header("x-easy-webhook-signature") ?? "",
process.env.EASY_WEBHOOK_SECRET!,
);
if (event.type === "checkout.session.completed") {
// event.data is the completed session — fulfill the order here.
}
res.status(204).end();
});Tradeoffs
- The iframe owns the look and feel within its bounds — you control the surrounding page, theme color, and logo (set via the merchant branding API), but the form layout itself is managed.
- For a fully bespoke UI where you render your own card form, see Build a custom checkout.
- Sessions are single-use. If the buyer abandons and comes back, create a new session.
- The iframe's allowed-origin list is a hard security boundary. Forgetting to add a new domain results in a
validatefailure inside the iframe with no charge attempted.