cimplify
Concepts

Checkout lifecycle

Every Cimplify checkout (embedded iframe, hosted Pay session, or controlled React Element) passes through the same ordered set of states. Whether you observe them via `onStatusChange`, the `checkout_status` postMessage event, or the `on_status_change` callback on `processCheckout()`, the names and order are identical.

The states

Defined by the CheckoutStatus union exported from @cimplify/sdk:

type CheckoutStatus =
  | "preparing"
  | "recovering"
  | "processing"
  | "awaiting_authorization"
  | "polling"
  | "finalizing"
  | "success"
  | "failed";

What each state means

StatusWhen it fires
preparingThe Element has received process_checkout and is gathering customer/payment context. Brief, usually a few hundred ms.
recoveringAn interrupted checkout was found in storage (page reloaded mid-flow) and is being resumed. Skipped on a fresh checkout.
processingThe order has been submitted to the API; the payment provider is being charged.
awaiting_authorizationThe provider returned a challenge: customer must enter an OTP, PIN, birthday, phone or address before the payment can complete. context.authorization_type tells you which.
pollingThe payment was authorized and the SDK is polling the order endpoint for the final settled state. context.poll_attempt and context.max_poll_attempts are populated.
finalizingPayment is captured; the order record is being written and webhooks dispatched.
successTerminal. The order is paid. context.order_id and context.order_number are populated.
failedTerminal. Customer can usually retry; error.recoverable on the final checkout_complete event tells you whether to keep the form or reset it.

Status context

Each transition carries a CheckoutStatusContext with optional fields. Use display_text for a human-readable status line; use the structured fields for progress UI.

interface CheckoutStatusContext {
  display_text?: string;
  authorization_type?: "otp" | "pin" | "phone" | "birthday" | "address";
  poll_attempt?: number;
  max_poll_attempts?: number;
  order_id?: string;
  order_number?: string;
  provider?: string;
}

Typical flow

mount → ready (parent posts init)

          [user fills form, presses Pay]

          preparing → processing

   ┌──────────────┴──────────────┐
   │                             │
   ▼                             ▼
awaiting_authorization      polling
   │                             │
   └──→ processing  ──→ polling ─┘

          finalizing

       success | failed (terminal)

Observing transitions

From a parent page (postMessage)

window.addEventListener("message", (event) => {
  if (event.data?.type === "checkout_status") {
    console.log(event.data.status, event.data.context);
  }
});

From CimplifyCheckout

<CimplifyCheckout
  client={client}
  cartId={cart.id}
  onStatusChange={(status, ctx) => {
    setStatusLabel(ctx.display_text ?? status);
  }}
  onComplete={(result) => {
    if (result.success) router.push(`/orders/${result.order!.id}`);
  }}
/>

From processCheckout()

const elements = client.elements(businessId);
const result = await elements.processCheckout({
  cart_id: cart.id,
  order_type: "delivery",
  on_status_change: (status, ctx) => {
    if (status === "awaiting_authorization") {
      // Show OTP/PIN entry tied to ctx.authorization_type
    }
  },
});

Terminal events

After success or failed, the iframe emits a single checkout_complete event with the full result. Stop reacting to checkout_status at that point; the iframe will not send further status updates for this checkout.

{
  type: "checkout_complete",
  success: true,
  order: {
    id: "ord_…",
    order_number: "#1042",
    status: "completed",
    total: "29.99",
    currency: "GHS"
  },
  enrolled_in_link: true
}

Recovery

If the customer reloads during awaiting_authorization or polling, the Element rehydrates from local storage and resumes. The first status you receive on the new page will be recovering, then back into the original state.

Next

On this page