Headless Checkout
Tier 3 -- full control over checkout UI and flow. A checkout element must be mounted for secure payment processing.
Even in headless mode, a checkout element must be mounted (it can be visually hidden). The element hosts the secure iframe that handles auth and payment processing internally.
TS
import { createCimplifyClient } from "@cimplify/sdk";
const client = createCimplifyClient({ publicKey: "pk_live_..." });
const elements = client.elements("bus_123", {
appearance: { theme: "light" },
});
// Mount checkout element (required for payment processing)
const checkout = elements.create("checkout", {
orderTypes: ["pickup", "delivery"],
});
checkout.mount("#checkout-container");
// Wait for checkout element to be ready
checkout.on("ready", async () => {
const result = await elements.processCheckout({
cart_id: "cart_123",
order_type: "delivery",
location_id: "loc_001",
enroll_in_link: true,
timeout_ms: 180_000,
on_status_change: (status, context) => {
updateUI(status, context.display_text);
},
});
if (result.success) {
window.location.href = "/order/" + result.order.id;
} else {
handleError(result.error);
}
});Backward compatibility: elements.create("payment") still works and processCheckout will use it as a fallback, but create("checkout") is preferred. The unified checkout element handles auth internally -- headless merchants don't need separate auth orchestration unless they want standalone sign-in.
processCheckout Options
| Option | Type | Description |
|---|---|---|
cart_id | string | Required. Cart to checkout. |
order_type | "delivery" | "pickup" | "dine_in" | Required. |
location_id? | string | Business location for the order. |
notes? | string | Order notes (e.g. "Ring doorbell"). |
tip_amount? | number | Tip amount in minor units. |
scheduled_time? | string | ISO 8601 datetime for scheduled orders. |
pay_currency? | string | Payment currency code (for multi-currency support). |
enroll_in_link? | boolean | Enroll customer in Cimplify Link. Defaults to true. |
timeout_ms? | number | Checkout timeout. Default 180000 (3 min). |
on_status_change? | (status, context) => void | Called at each status transition. |
Status Lifecycle
TS
// Typical flow:
// preparing → processing → awaiting_authorization → polling → finalizing → success
// → failed
// Recovery (after page refresh during authorization):
// recovering → polling → finalizing → success | failed
const statusMessages: Record<string, string> = {
preparing: "Setting up your order...",
recovering: "Resuming your checkout...",
processing: "Processing payment...",
awaiting_authorization: "Waiting for authorization...",
polling: "Confirming with payment provider...",
finalizing: "Almost done...",
success: "Order placed!",
failed: "Payment failed.",
};
elements.processCheckout({
cart_id: "cart_123",
order_type: "pickup",
on_status_change: (status, context) => {
document.getElementById("status").textContent =
context.display_text || statusMessages[status] || status;
},
});AbortablePromise
processCheckout returns an AbortablePromise -- a standard Promise with an abort() method to cancel the in-flight checkout.
TS
const checkout = elements.processCheckout({
cart_id: "cart_123",
order_type: "delivery",
});
// Cancel checkout (e.g., user navigates away)
checkout.abort();
// The awaited result after abort:
// { success: false, error: { code: "CHECKOUT_ABORTED", message: "..." } }Error Recovery
TS
const result = await elements.processCheckout({
cart_id: "cart_123",
order_type: "pickup",
on_status_change: (status) => updateUI(status),
});
if (!result.success) {
const { code, message, recoverable } = result.error;
if (recoverable) {
// Safe to retry -- show retry button
showRetryUI(message);
} else {
// Terminal failure -- show error and redirect
showError(message);
}
}Complete Custom Form Example
TS
import { createCimplifyClient } from "@cimplify/sdk";
async function customCheckout() {
const client = createCimplifyClient({ publicKey: "pk_live_..." });
const elements = client.elements("bus_123");
// Mount checkout element -- handles auth, address, and payment internally
const checkout = elements.create("checkout", {
orderTypes: ["pickup", "delivery"],
});
checkout.mount("#checkout-slot");
// Collect your own form data
const form = document.getElementById("checkout-form") as HTMLFormElement;
const formData = new FormData(form);
await new Promise<void>((resolve) => {
checkout.on("ready", () => resolve());
});
const checkoutResult = elements.processCheckout({
cart_id: formData.get("cartId") as string,
order_type: formData.get("orderType") as string,
location_id: formData.get("locationId") as string,
enroll_in_link: true,
timeout_ms: 120_000,
on_status_change: (status, context) => {
document.getElementById("progress").textContent =
context.display_text || status;
},
});
try {
const result = await checkoutResult;
if (result.success) {
window.location.href = "/confirmation/" + result.order.id;
} else {
document.getElementById("error").textContent = result.error.message;
}
} finally {
elements.destroy();
}
}
document.getElementById("pay-btn").onclick = () => customCheckout();