Easy Labs
PaymentsGuides

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/node for the server, @easylabs/react (or @easylabs/browser for 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_origins config. Set it once with client.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();

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 validate failure inside the iframe with no charge attempted.

On this page