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
When the shopper enters a valid contact and chooses Cimplify, checkout emits:
{
type: "auth_requested",
loginHint: "jane@example.com",
contactType: "email",
mode: "checkout",
nonce: "ab12cd34"
}A raw iframe parent must do one of these:
- Use
@cimplify/sdkjust for the checkout controller orstartSignIn, which handles OAuth, PKCE,web_message, callback exchange, andset_token. - Implement the OAuth bridge yourself.
The successful end of the flow is always:
iframe.contentWindow!.postMessage({
type: "set_token",
token: accessTokenFromYourCallback,
}, LINK_ORIGIN);For a delightful checkout, do not show a separate "are you a member?" step and
do not reveal account existence. Let checkout collect the contact, then start
OAuth with that contact as login_hint. Cimplify will silently use an existing
matching SSO session, or show OTP for that contact.
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.