cimplify
Checkout integration

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 getYou 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

On this page