Vanilla Elements
The framework-agnostic checkout surface. Use this from Vue, Svelte, plain HTML, or anywhere else React isn't the right tool. The same controller backs the React wrappers; they're just convenience around `CimplifyElements`.
What ships
From @cimplify/sdk:
import {
createCimplifyClient,
createElements, // factory
CimplifyElements, // the controller class
CimplifyElement, // a single mounted element
ELEMENT_TYPES, // "auth" | "address" | "payment" | "checkout" | "account"
EVENT_TYPES, // "ready" | "authenticated" | "change" | …
MESSAGE_TYPES, // postMessage type constants
} from "@cimplify/sdk";Bootstrap
const client = createCimplifyClient({
publicKey: "pk_live_…",
});
// businessId is optional; the controller resolves it from the public key.
const elements = createElements(client, "biz_01J5…", {
appearance: {
theme: "light",
variables: { primaryColor: "#059669", borderRadius: "0.85rem" },
},
});Mounting an element
Every element type lives at link.cimplify.io/elements/<type>. The controller creates the iframe, hooks up postMessage, and routes events.
const checkout = elements.create(ELEMENT_TYPES.CHECKOUT, {
orderTypes: ["delivery", "pickup"],
defaultOrderType: "delivery",
submitLabel: "Pay GH₵29.99",
});
checkout.on(EVENT_TYPES.READY, () => console.log("checkout iframe ready"));
checkout.on(EVENT_TYPES.ERROR, (err) => console.error(err));
checkout.mount("#cimplify-checkout"); // selector or HTMLElementElement types
| Constant | Iframe path | What it renders |
|---|---|---|
ELEMENT_TYPES.AUTH | /elements/auth | Single-line OTP sign-in. |
ELEMENT_TYPES.ADDRESS | /elements/address | Address picker / form. |
ELEMENT_TYPES.PAYMENT | /elements/payment | Alias of checkout. |
ELEMENT_TYPES.CHECKOUT | /elements/checkout | Full unified checkout (auth + address + payment + submit). |
ELEMENT_TYPES.ACCOUNT | /elements/account/* | Logged-in account portal. |
Event types
Subscribe via element.on(eventType, handler). EVENT_TYPES covers:
| Event | Payload | Fires on |
|---|---|---|
READY | { height } | iframe ready |
AUTHENTICATED | AuthenticatedData | OTP success (Auth) |
REQUIRES_OTP | { contactMasked } | OTP dispatched (Auth) |
CHANGE | { address } or { paymentMethod } | field changes (Address / Payment) |
ORDER_TYPE_CHANGED | { orderType } | delivery/pickup toggle (Checkout) |
REQUEST_SUBMIT | {} | in-iframe Pay button pressed |
ERROR | { code, message } | any failure |
Pushing the cart
checkout.setCart({
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",
});Processing checkout
processCheckout returns an AbortablePromise; call .abort() to cancel mid-flow. The promise settles only on terminal checkout_complete or timeout.
const task = elements.processCheckout({
cart_id: cart.id,
order_type: "delivery",
enroll_in_link: true,
on_status_change: (status, ctx) => {
setStatusLine(ctx.display_text ?? status);
},
});
// Cancel button:
cancelBtn.addEventListener("click", () => task.abort());
const result = await task;
if (result.success) {
location.assign(`/orders/${result.order!.id}`);
} else {
console.error(result.error?.code, result.error?.message);
}In-iframe submit button
When you create the element with no explicit renderSubmitButton, the iframe renders its own Pay button. Listen for REQUEST_SUBMIT and call processCheckout:
checkout.on(EVENT_TYPES.REQUEST_SUBMIT, async () => {
const result = await elements.processCheckout({
cart_id: cart.id,
order_type: "delivery",
});
// …
});Cleanup
Call destroy() when leaving the page or unmounting the host. This removes the iframe, clears all event handlers, and removes the postMessage listener.
// On route leave:
elements.destroy(); // tears down all elements at once
// or per-element:
checkout.destroy();Full example (vanilla TS)
import {
createCimplifyClient,
createElements,
ELEMENT_TYPES,
EVENT_TYPES,
} from "@cimplify/sdk";
const client = createCimplifyClient({ publicKey: "pk_live_…" });
const elements = createElements(client);
const checkout = elements.create(ELEMENT_TYPES.CHECKOUT, {
defaultOrderType: "delivery",
});
checkout.on(EVENT_TYPES.READY, async () => {
const cart = await client.cart.get();
if (cart.ok) {
checkout.setCart(toCheckoutCart(cart.value));
}
});
checkout.on(EVENT_TYPES.REQUEST_SUBMIT, async () => {
const cart = await client.cart.get();
if (!cart.ok) return;
const result = await elements.processCheckout({
cart_id: cart.value.id,
order_type: "delivery",
});
if (result.success) {
location.assign(`/orders/${result.order!.id}`);
} else {
showError(result.error?.message ?? "Checkout failed");
}
});
checkout.mount("#checkout-container");
// Later:
window.addEventListener("beforeunload", () => elements.destroy());Next
- React wrappers: Same controller, but as components
- Element events: The full postMessage protocol
Controlled Elements (React)
React wrappers around the Cimplify Element iframes. Drop a component into your tree, pass a few props, and you have a working checkout. The iframe still does all the rendering; you just orchestrate it with React.
Headless checkout
Drive the checkout API directly. No iframe, no Cimplify-rendered fields; every screen, every input, every microcopy is yours. Use this when the iframe's look or layout can't accommodate your brand, or when you're building a non-web surface (native app, kiosk, voice).