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
| Param | Required | What it does |
|---|---|---|
businessId | yes | The Cimplify business this checkout belongs to. |
nonce | recommended | Per-iframe random string. The iframe echoes it on every message. |
email | no | Pre-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
nonceand verify it on incoming messages. - Send messages to
"https://link.cimplify.io", not"*". - Include
allow-same-originandallow-popupsin 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
- Element events: Full message catalog
- Vanilla Elements: SDK-managed mount, no React
Drop-in checkout (hosted Pay)
The fastest path to a paid order: create a checkout session on your server, then redirect the customer to the URL it returns. Cimplify hosts the entire checkout UI at `pay.cimplify.io/s/<sessionId>` (auth, address, payment method, compliance). You get a webhook (or success-URL redirect) when payment lands.
CimplifyCheckout (React)
Mount the Cimplify checkout iframe from React. This is the recommended embedded checkout path for agent-built storefronts and custom merchant sites.