cimplify

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

OptionTypeDescription
cart_idstringRequired. Cart to checkout.
order_type"delivery" | "pickup" | "dine_in"Required.
location_id?stringBusiness location for the order.
notes?stringOrder notes (e.g. "Ring doorbell").
tip_amount?numberTip amount in minor units.
scheduled_time?stringISO 8601 datetime for scheduled orders.
pay_currency?stringPayment currency code (for multi-currency support).
enroll_in_link?booleanEnroll customer in Cimplify Link. Defaults to true.
timeout_ms?numberCheckout timeout. Default 180000 (3 min).
on_status_change?(status, context) => voidCalled 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();

Next Steps