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).
Two starting points
- High-level:
<CheckoutPage>from@cimplify/sdk/react, a full, opinionated checkout screen built from the regular React component primitives. Style it via Tailwind / classNames; ejectable. - Fully custom: Call
client.checkout.process(...)from whatever UI you build. The SDK handles polling, status, and recovery; you handle every pixel.
Using <CheckoutPage>
The fastest way to a fully-custom-looking checkout. CheckoutPage renders the whole flow as React components (no iframe), reads the cart from useCart(), and dispatches to client.checkout.process on submit.
import { CimplifyProvider, CheckoutPage } from "@cimplify/sdk/react";
import { createCimplifyClient } from "@cimplify/sdk";
const client = createCimplifyClient({
publicKey: process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY!,
});
export default function Checkout() {
return (
<CimplifyProvider client={client}>
<CheckoutPage />
</CimplifyProvider>
);
}Style it with Tailwind classes on the wrapping container, override sub-components via the registry (cimplify add checkout-page to eject), or skip it entirely and go fully custom.
Fully custom
Build whatever UI you want and call client.checkout.process on submit. The body is flat; see the checkout SDK reference for the full CheckoutFormData shape.
async function handleSubmit(form: MyFormState) {
const cart = await client.cart.get();
if (!cart.ok) {
setError(cart.error.message);
return;
}
const r = await client.checkout.process({
cart_id: cart.value.id,
order_type: form.orderType, // "delivery" | "pickup" | "dine_in"
payment_method: form.paymentMethod, // "mobile_money" | "card"
customer: {
name: form.name,
email: form.email,
phone: form.phone,
save_details: form.rememberMe,
},
address_info: form.orderType === "delivery" ? {
street_address: form.street,
city: form.city,
region: form.region,
} : undefined,
mobile_money_details: form.paymentMethod === "mobile_money" ? {
phone_number: form.momoNumber,
provider: form.momoProvider,
} : undefined,
special_instructions: form.notes,
});
if (!r.ok) {
setError(`${r.error.code}: ${r.error.message}`);
return;
}
// r.value: { order_id, order_number, payment_status, requires_authorization, next_action, ... }
if (r.value.requires_authorization) {
// Show OTP / PIN screen, then call client.checkout.submitAuthorization(...)
} else {
router.push(`/orders/${r.value.order_id}`);
}
}next_action
The response carries a discriminated next_action describing what to do next: redirect to a 3DS / provider page, poll for status, or nothing. Branch on the type; the SDK reference covers each variant.
switch (r.value.next_action?.type) {
case "redirect":
window.location.assign(r.value.next_action.url);
break;
case "poll":
pollUntilDone(r.value.order_id);
break;
case "none":
default:
router.push(`/orders/${r.value.order_id}`);
}Polling
For payment methods that settle asynchronously, use client.checkout.pollPaymentStatus. It re-queries the order until terminal state.
async function pollUntilDone(orderId: string) {
for (let i = 0; i < 30; i++) {
const r = await client.checkout.pollPaymentStatus(orderId);
if (!r.ok) throw new Error(r.error.message);
if (r.value.payment_status === "success") return r.value;
if (r.value.payment_status === "failed") throw new Error("Payment failed");
await new Promise(res => setTimeout(res, 2000));
}
throw new Error("Timed out");
}Trade-offs vs Elements
| You get | You give up |
|---|---|
| Full visual / UX control. | You implement OTP, address validation, payment provider error handling. |
| Native-app friendly (no iframe). | You handle PCI / OTP compliance scope. |
| Server-side rendering / a11y / i18n on your terms. | No automatic theme via Appearance API. |
| Custom telemetry hooks anywhere. | You wire up the full lifecycle UI. |
Next
- checkout SDK reference: Every field on
CheckoutFormData - Controlled Elements: When you want to keep the iframe
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`.
Appearance API
Every Cimplify Element iframe (`AuthElement`, `CheckoutElement`, `AccountElement`, the hosted Pay page) accepts an `appearance` object that controls theme, accent color, font, and corner radius. The same shape works across all integration tiers.