cimplify
Checkout integration

Embedded checkout iframe

Mount the same checkout UI as Cimplify Pay on your own domain. Raw iframe embedding is possible, but saved-details sign-in requires an OAuth bridge; most storefronts should use the SDK controller.

The embedded iframe is the lowest-level checkout integration. It is useful for Vue, Svelte, plain HTML, webviews, or custom platforms. If you are building a React storefront, prefer CimplifyCheckout.

Guest checkout can be wired with plain postMessage. Cimplify saved details require handling the checkout auth request and returning a token with set_token. The SDK already does that bridge.

Iframe URL

https://link.cimplify.io/elements/checkout?businessId=biz_…&nonce=<random>

/elements/payment is an alias of /elements/checkout.

URL Params

ParamRequiredWhat it does
businessIdyesThe Cimplify business this checkout belongs to.
noncerecommendedPer-iframe random string. The iframe echoes it on every message.
emailnoPre-fill the checkout contact field.

Minimal Embed

<iframe
  id="cimplify-checkout"
  src="https://link.cimplify.io/elements/checkout?businessId=biz_01J5…&nonce=ab12cd34"
  style="border:0; width:100%; min-height:200px; display:block; background:transparent"
  allowtransparency="true"
  allow="geolocation"
  sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
></iframe>

Init Handshake

The iframe posts { type: "ready", height, nonce }. Reply with init.

const iframe = document.getElementById("cimplify-checkout") as HTMLIFrameElement;
const LINK_ORIGIN = "https://link.cimplify.io";
const nonce = "ab12cd34";

window.addEventListener("message", (event) => {
  if (event.origin !== LINK_ORIGIN) return;
  if (event.source !== iframe.contentWindow) return;
  if (event.data?.nonce !== nonce) return;

  if (event.data?.type === "ready") {
    iframe.contentWindow!.postMessage({
      type: "init",
      businessId: "biz_01J5…",
      publicKey: "cpk_live_…",
      appearance: {
        theme: "light",
        variables: { primaryColor: "#059669", borderRadius: "0.85rem" },
      },
      orderTypes: ["delivery", "pickup"],
      defaultOrderType: "delivery",
      renderSubmitButton: true,
      submitLabel: "Pay GH₵29.99",
    }, LINK_ORIGIN);

    iframe.style.height = event.data.height + "px";
  }
});

Auto-Resize

window.addEventListener("message", (event) => {
  if (event.origin !== LINK_ORIGIN) return;
  if (event.source !== iframe.contentWindow) return;
  if (event.data?.nonce !== nonce) return;

  if (event.data?.type === "height_change") {
    iframe.style.height = event.data.height + "px";
  }
});

Pushing the Cart

Send set_cart to render an order summary.

iframe.contentWindow!.postMessage({
  type: "set_cart",
  cart: {
    items: [
      {
        name: "Jollof bowl",
        quantity: 1,
        unit_price: "29.99",
        total_price: "29.99",
        line_type: "simple",
      },
    ],
    subtotal: "29.99",
    tax_amount: "0.00",
    total_discounts: "0.00",
    service_charge: "0.00",
    total: "29.99",
    currency: "GHS",
  },
}, LINK_ORIGIN);

Handling Saved-Details Sign-In

Nothing is required. Checkout recognizes the contact, runs the OTP challenge in-panel, and hydrates the shopper's saved details on its own — a raw iframe parent that ignores identity messages still gets the full checkout.

To keep the shopper recognized on their next visit, handle the handoff checkout emits after a successful verification:

{
  type: "session_established",
  accessToken: "…",
  refreshToken: "…",
  nonce: "ab12cd34"
}

The @cimplify/sdk elements controller does this for you: it claims the storefront session so cart and chat share the verified identity, persists the refresh token first-party, and echoes set_token. A hand-rolled parent that wants the same durability should store the refresh token in first-party storage and restore it through the SDK on the next visit.

Do not show a separate "are you a member?" step — checkout never reveals account existence before OTP verification.

Starting Checkout

With renderSubmitButton: true, the iframe renders its own Pay button and emits request_submit. Reply with process_checkout.

window.addEventListener("message", (event) => {
  if (event.origin !== LINK_ORIGIN) return;
  if (event.source !== iframe.contentWindow) return;
  if (event.data?.nonce !== nonce) return;

  if (event.data?.type === "request_submit") {
    iframe.contentWindow!.postMessage({
      type: "process_checkout",
      cart_id: cart.id,
      order_type: "delivery",
      enroll_in_link: true,
    }, LINK_ORIGIN);
  }

  if (event.data?.type === "checkout_complete") {
    if (event.data.success) {
      window.location.href = `/orders/${event.data.order.id}`;
    } else {
      console.error(event.data.error);
    }
  }
});

Security

  • Always check event.origin.
  • Always check event.source.
  • Use a random per-iframe nonce and verify it on incoming messages.
  • Send messages to "https://link.cimplify.io", not "*".
  • Include allow-same-origin and allow-popups in the sandbox. Link sessions and some payment providers need them.
  • Post only the short-lived access token returned by your callback.

When to Graduate

Once you are writing more than a small message handler, switch to CimplifyCheckout or Vanilla checkout. The SDK controller handles auth, origin checks, nonce routing, token broadcast, checkout processing, aborts, and timeouts.

Next

On this page