> ## Documentation Index
> Fetch the complete documentation index at: https://www.commercengine.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Checkout

> This guide walks you through the process of converting a customer's shopping cart into a placed order, including address validation, shipping checks, payment processing, and handling payment retries. It utilizes endpoints primarily from the Carts, Orders, Payments, Shipping, and Common modules.

Commerce Engine provides two checkout paths:

<CardGroup cols={2}>
  <Card title="Hosted Checkout" icon="bag-shopping" href="/docs/hosted-checkout/overview">
    Recommended. Pre-built cart, authentication, addresses, fulfilment, discounts, payment, and order confirmation inside an embeddable checkout.
  </Card>

  <Card title="Custom checkout" icon="code">
    Advanced. Use the Storefront SDK when the business requires complete control over the checkout UI and payment experience.
  </Card>
</CardGroup>

Do not build custom checkout only to change colours, typography, layout direction, login methods, or feature flags. Configure those through [Checkout Studio](/docs/hosted-checkout/checkout-studio).

## Hosted Checkout path

Install the checkout package and use the framework-specific integration.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @commercengine/checkout
```

When the application also uses `@commercengine/storefront`, use `authMode: "provided"` and two-way token synchronization. This preserves one user, cart, and analytics journey.

Hosted Checkout handles:

* cart review and quantity updates
* OTP login/registration
* saved and new addresses
* delivery and collect-in-store options where configured
* coupons, promotions, loyalty, and credit features
* payment-provider UI
* order completion and callbacks

The host application should handle `onReady`, `onError`, `onSessionError`, and `onComplete`, and keep cart/checkout controls disabled until checkout is ready.

## Custom checkout prerequisites

A custom checkout requires:

* an active cart with at least one item
* a logged-in customer session
* billing and shipping addresses as applicable
* a valid fulfilment preference for physical goods
* an available payment method unless `to_be_paid` is zero

Recommended journey:

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Cart → OTP login/register → address → deliverability → fulfilment
→ discounts → payment method → create order → payment → status → confirmation
```

Authentication before address/payment verifies the contact channel and reduces delivery failures. Use `register_if_not_exists: true` for a single login/register path.

## 1. Review the current cart

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { data, error, response } = await sdk.cart.getUserCart();

if (response?.status === 404) {
  // Return to shopping; this session has no active cart.
  return;
}

if (error) throw new Error(error.message);
if (!data?.cart) throw new Error("Cart response did not include a cart");

const cart = data.cart;
```

Before continuing:

* ensure the cart is active and not expired
* revalidate item availability
* display `to_be_paid` as the final payable amount
* show promotions, coupons, loyalty, credit, tax, and shipping separately

## 2. Authenticate

If `sdk.isLoggedIn()` is false, complete OTP or password authentication before entering the protected checkout steps.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const loggedIn = await sdk.isLoggedIn();
if (!loggedIn) {
  // Show OTP login or open Hosted Checkout auth.
}
```

After login, refetch the cart. Commerce Engine merges the active anonymous cart; do not create a replacement cart.

## 3. Create or select an address

For a custom checkout, create addresses on the customer profile and link their IDs to the cart. This preserves them for later orders.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { data: addressData, error: addressError } =
  await sdk.customer.createAddress({
    first_name: "Asha",
    last_name: "Sharma",
    email: "asha@example.com",
    phone: "9876543210",
    country_code: "+91",
    address_line1: "123 Market Road",
    address_line2: "",
    city: "Mumbai",
    state: "Maharashtra",
    pincode: "400001",
    country: "India",
  });

if (addressError) throw new Error(addressError.message);
const addressId = addressData?.address?.id;
```

Confirm the exact current `createAddress` body type from the LLM-first reference. Address fields can vary by country and operation version.

Link the selected addresses:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { data: updatedCart, error } = await sdk.cart.updateCartAddress(
  { id: cart.id },
  {
    shipping_address_id: addressId,
    billing_address_id: addressId,
  },
);

if (error) throw new Error(error.message);
```

Use the returned cart because shipping and tax totals may change.

### Pincode lookup

For supported countries, use the helper operation to suggest city/state after the customer enters a valid-length pincode. Debounce the lookup and allow manual correction if no result is found.

Do not treat an address lookup as a deliverability check.

## 4. Check deliverability

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { data, error } = await sdk.cart.checkPincodeDeliverability({
  cart_id: cart.id,
  delivery_pincode: selectedAddress.pincode,
});

if (error) throw new Error(error.message);

if (!data?.is_serviceable) {
  // Show unserviceable items and request another address.
}
```

Use `unserviceable_items` where returned to explain whether the whole cart or only specific items cannot be delivered.

Do not use legacy shipment serviceability endpoints or guess a path; retrieve the exact current operation contract.

## 5. Retrieve fulfilment options

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { data, error } = await sdk.cart.getFulfillmentOptions({
  cart_id: cart.id,
});

if (error) throw new Error(error.message);
```

The response can include:

* summary availability and recommendation
* delivery shipments and shipping methods
* courier options for an aggregator method
* collect-in-store locations

A cart can produce multiple shipments. The UI must choose a valid method for every shipment rather than assuming one shipping option covers the order.

## 6. Set fulfilment preference

Delivery example:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { data, error } = await sdk.cart.updateFulfillmentPreference(
  { id: cart.id },
  {
    fulfillment_type: "delivery",
    shipments: selections.map((selection) => ({
      id: selection.shipmentId,
      shipping_provider_id: selection.shippingMethodId,
      ...(selection.courierId
        ? { courier_company_id: selection.courierId }
        : {}),
    })),
  },
);
```

Collect-in-store example:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
await sdk.cart.updateFulfillmentPreference(
  { id: cart.id },
  {
    fulfillment_type: "collect-in-store",
    pickup_location_id: selectedLocationId,
  },
);
```

Restore the cart's existing preference after refresh instead of automatically replacing the customer's previous selection.

If the product requirements allow automatic delivery selection, choose using an explicit rule—for example, lowest cost then shortest delivery estimate—and show the selection to the customer.

<Frame caption="Address, deliverability, and fulfilment">
  <iframe title="Address, deliverability, and fulfilment" allowFullScreen width="100%" height="760" src="https://mermaid.tarkai.dev/embed?definition=sequenceDiagram%0A%20%20%20%20participant%20User%0A%20%20%20%20participant%20UI%20as%20Custom%20Checkout%20UI%0A%20%20%20%20participant%20SDK%20as%20Session%20Storefront%20SDK%0A%20%20%20%20participant%20CE%20as%20Commerce%20Engine%20API%0A%20%20%20%20User-%3E%3EUI%3A%20Selects%20or%20creates%20address%0A%20%20%20%20UI-%3E%3ESDK%3A%20updateCartAddress%28address%20ids%29%0A%20%20%20%20SDK-%3E%3ECE%3A%20POST%20%2Fcarts%2F%7Bid%7D%2Faddress%0A%20%20%20%20CE--%3E%3ESDK%3A%20Cart%20with%20recalculated%20totals%0A%20%20%20%20SDK--%3E%3EUI%3A%20Re-render%20totals%0A%20%20%20%20UI-%3E%3ESDK%3A%20checkPincodeDeliverability%28cart_id%2C%20pincode%29%0A%20%20%20%20SDK-%3E%3ECE%3A%20POST%20%2Ffulfillment%2Fserviceability%0A%20%20%20%20alt%20Cart%20serviceable%0A%20%20%20%20%20%20%20%20CE--%3E%3ESDK%3A%20Serviceable%20%2B%20affected-item%20details%0A%20%20%20%20%20%20%20%20UI-%3E%3ESDK%3A%20getFulfillmentOptions%28cart_id%29%0A%20%20%20%20%20%20%20%20SDK-%3E%3ECE%3A%20POST%20%2Fcarts%2Ffulfillment-options%0A%20%20%20%20%20%20%20%20CE--%3E%3ESDK%3A%20Shipments%2C%20methods%2C%20pickup%20locations%0A%20%20%20%20%20%20%20%20User-%3E%3EUI%3A%20Chooses%20option%20for%20every%20shipment%0A%20%20%20%20%20%20%20%20UI-%3E%3ESDK%3A%20updateFulfillmentPreference%28selection%29%0A%20%20%20%20%20%20%20%20SDK-%3E%3ECE%3A%20POST%20%2Fcarts%2F%7Bid%7D%2Ffulfillment-preference%0A%20%20%20%20%20%20%20%20CE--%3E%3ESDK%3A%20Updated%20Cart%0A%20%20%20%20%20%20%20%20SDK--%3E%3EUI%3A%20Continue%20to%20payment%0A%20%20%20%20else%20Not%20serviceable%0A%20%20%20%20%20%20%20%20CE--%3E%3ESDK%3A%20Not%20serviceable%20%2B%20affected%20items%0A%20%20%20%20%20%20%20%20SDK--%3E%3EUI%3A%20Request%20another%20address%20or%20item%20removal%0A%20%20%20%20end" />
</Frame>

## 7. Apply discounts

Coupons, loyalty points, and credit balance update the cart. Use the returned cart after every operation and display the new `to_be_paid`.

A 100% discount or sufficient credit can produce `to_be_paid: 0`; order creation may then complete without a payment redirect.

## 8. Discover and validate payment methods

For a custom payment UI, retrieve the store's available methods rather than hardcoding cards, UPI, banks, or wallets.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { data, error } = await sdk.payments.listPaymentMethods({
  payment_provider_slug: "juspay",
});

if (error) throw new Error(error.message);
```

Payment method availability and payload fields are provider- and integration-specific. Use the exact slug returned by payment-method discovery; the current Juspay contract examples use lowercase `juspay`. Retrieve the exact method and schema contracts from the LLM-first reference.

For express payment UIs:

* use card BIN lookup before declaring a network supported
* validate UPI VPA before submission where applicable
* do not store or log raw card/OTP data
* prefer provider-hosted surfaces when PCI scope and complexity are not justified

## 9. Create the order

Hyper Checkout example:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { data, error } = await sdk.order.createOrder({
  cart_id: cart.id,
  payment_method: {
    payment_provider_slug: "juspay",
    integration_type: "hyper-checkout",
    action: "paymentPage",
    gateway_reference_id: gatewayReferenceId,
    return_url: `${window.location.origin}/checkout/return`,
  },
});

if (error) throw new Error(error.message);
```

Use the exact provider slug, action, and payload casing returned/documented for the configured integration. Do not copy placeholder gateway IDs into production.

Persist the returned order number before redirecting.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const orderNumber = data?.order?.order_number;
if (!orderNumber) throw new Error("Order response did not include an order number");

sessionStorage.setItem("pending_order_number", orderNumber);
```

Handle both outcomes:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
if (!data?.payment_required) {
  showOrderConfirmation(data?.order);
} else {
  window.location.assign(data.payment_info.payment_links.web);
}
```

Confirm the payment-info shape for the selected provider before accessing a link.

## 10. Verify payment status

Do not trust redirect query parameters as the authoritative payment result. Commerce Engine processes provider callbacks asynchronously.

Poll the order status after the customer returns:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const MAX_POLLS = 40;
const POLL_INTERVAL_MS = 3_000;

async function waitForPayment(orderNumber: string) {
  for (let attempt = 0; attempt < MAX_POLLS; attempt += 1) {
    const { data, error, response } =
      await sdk.order.getPaymentStatus(orderNumber);

    if (error) {
      const status = response?.status;
      const retryable =
        status === undefined ||
        status === 408 ||
        status === 429 ||
        (status >= 500 && status < 600);

      if (!retryable) throw new Error(error.message);

      await new Promise((resolve) =>
        setTimeout(resolve, POLL_INTERVAL_MS),
      );
      continue;
    }

    if (!data) throw new Error("Payment status response was empty");
    if (data.status === "success" || data.status === "failed") return data;

    if (
      data.status !== "pending" &&
      data.status !== "partially_paid"
    ) {
      return data;
    }

    await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
  }

  return null;
}
```

If the poll times out, show a processing state and link to order history. Do not declare failure or success prematurely.

## 11. Retry a failed payment

Only show Retry when `is_retry_available` is true.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { data, error } = await sdk.order.retryOrderPayment(
  { order_number: orderNumber },
  {
    payment_method: {
      payment_provider_slug: "juspay",
      integration_type: "hyper-checkout",
      action: "paymentPage",
      gateway_reference_id: gatewayReferenceId,
      return_url: `${window.location.origin}/checkout/return`,
    },
  },
);

if (error) throw new Error(error.message);
```

Use the new payment information and repeat the authoritative status check.

Prevent duplicate clicks while retry creation is in flight.

<Frame caption="Order creation, payment verification, and retry">
  <iframe title="Order creation, payment verification, and retry" allowFullScreen width="100%" height="900" src="https://mermaid.tarkai.dev/embed?definition=sequenceDiagram%0A%20%20%20%20participant%20User%0A%20%20%20%20participant%20UI%20as%20Storefront%0A%20%20%20%20participant%20SDK%20as%20Storefront%20SDK%0A%20%20%20%20participant%20CE%20as%20Commerce%20Engine%0A%20%20%20%20participant%20PG%20as%20Payment%20Provider%0A%20%20%20%20User-%3E%3EUI%3A%20Confirm%20order%0A%20%20%20%20UI-%3E%3ESDK%3A%20createOrder%28cart%2C%20payment%20method%29%0A%20%20%20%20SDK-%3E%3ECE%3A%20POST%20%2Forders%0A%20%20%20%20CE--%3E%3ESDK%3A%20order%20%2B%20payment%20info%0A%20%20%20%20UI-%3E%3EUI%3A%20Persist%20order%20number%0A%20%20%20%20alt%20No%20payment%20required%0A%20%20%20%20%20%20%20%20UI--%3E%3EUser%3A%20Order%20confirmation%0A%20%20%20%20else%20Payment%20required%0A%20%20%20%20%20%20%20%20UI--%3E%3EUser%3A%20Open%20provider%20flow%0A%20%20%20%20%20%20%20%20User-%3E%3EPG%3A%20Attempt%20payment%0A%20%20%20%20%20%20%20%20PG--%3E%3EUser%3A%20Return%20to%20storefront%0A%20%20%20%20%20%20%20%20loop%20Pending%20within%20timeout%0A%20%20%20%20%20%20%20%20%20%20%20%20UI-%3E%3ESDK%3A%20getPaymentStatus%28order%20number%29%0A%20%20%20%20%20%20%20%20%20%20%20%20SDK-%3E%3ECE%3A%20GET%20payment%20status%0A%20%20%20%20%20%20%20%20%20%20%20%20CE--%3E%3ESDK%3A%20Authoritative%20status%0A%20%20%20%20%20%20%20%20end%0A%20%20%20%20%20%20%20%20alt%20Success%0A%20%20%20%20%20%20%20%20%20%20%20%20UI--%3E%3EUser%3A%20Confirmed%20order%0A%20%20%20%20%20%20%20%20else%20Failed%20and%20retry%20available%0A%20%20%20%20%20%20%20%20%20%20%20%20User-%3E%3EUI%3A%20Retry%20payment%0A%20%20%20%20%20%20%20%20%20%20%20%20UI-%3E%3ESDK%3A%20retryOrderPayment%28order%20number%29%0A%20%20%20%20%20%20%20%20%20%20%20%20SDK-%3E%3ECE%3A%20POST%20retry-payment%0A%20%20%20%20%20%20%20%20%20%20%20%20CE--%3E%3ESDK%3A%20New%20payment%20info%0A%20%20%20%20%20%20%20%20%20%20%20%20UI--%3E%3EUser%3A%20Start%20new%20provider%20flow%0A%20%20%20%20%20%20%20%20else%20Pending%20or%20no%20retry%0A%20%20%20%20%20%20%20%20%20%20%20%20UI--%3E%3EUser%3A%20Recovery%20state%20%2B%20order%20history%0A%20%20%20%20%20%20%20%20end%0A%20%20%20%20end" />
</Frame>

## Payment-return resilience

The return page should recover from:

* missing query parameters
* browser refresh
* a new tab
* delayed provider callback
* network failure during polling
* customer navigating away and returning through order history

Persist the order number before redirecting and verify that it belongs to the current authenticated session before displaying sensitive details.

## Error handling

| Stage | Common recovery |
| - | - |
| Cart expired | Return to cart; recreate on next add |
| Login challenge expired | Request a new OTP challenge |
| Address validation failed | Preserve form input; focus the invalid field |
| Pincode unserviceable | Select another address or remove affected items |
| Fulfilment changed | Refetch options and require a new valid selection |
| Payment method unsupported | Select another discovered method |
| Create order ambiguous | Check current cart/order state before retrying |
| Payment pending | Continue bounded polling; show processing state |
| Payment failed | Retry only when allowed |

Do not blindly retry order creation. An ambiguous response can still mean an order was created.

## Accessibility and UX

* preserve checkout step state after recoverable errors
* focus the first invalid field
* announce total and fulfilment changes
* prevent accidental double submission
* show progress without trapping keyboard focus
* provide clear processing, success, failed, and pending states
* keep order confirmation accessible after refresh through account history

Hosted Checkout supplies these screens; custom checkout teams must build and test them.

## Analytics in a custom checkout

`@commercengine/analytics` is not tied to Hosted Checkout. A custom checkout built with the Storefront SDK or direct REST can map the same canonical Commerce Engine `Cart`, `CartItem`, `Order`, and user entities into Segment/RudderStack-compatible events.

The application owns the semantic trigger—for example, emit `Checkout Started` when the customer actually begins checkout—while the package owns the event schema and entity normalization.

<Card title="Commerce analytics" icon="chart-line" href="/docs/hosted-checkout/analytics">
  Use the vendor-neutral mapper package with Hosted Checkout, custom SDK checkout, direct REST, any compatible CDP, or a custom analytics pipeline.
</Card>

## Production checklist

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
[ ] Hosted Checkout considered before custom build
[ ] user logged in before custom checkout
[ ] active cart refetched after login
[ ] saved address created/selected and linked by ID
[ ] returned cart totals used after address update
[ ] deliverability checked with current cart operation
[ ] every shipment has a valid fulfilment selection
[ ] existing preference restored on refresh
[ ] payment methods discovered, not hardcoded
[ ] order number persisted before redirect
[ ] payment status verified through Commerce Engine
[ ] pending status has bounded polling and recovery UI
[ ] retry shown only when allowed
[ ] duplicate order/payment submissions prevented
[ ] checkout analytics events have one owner and use canonical CE entities
[ ] provider credentials and payment data never logged
```

<Card title="Custom checkout skills" icon="github" href="https://github.com/commercengine/skills">
  The cart-checkout skill contains detailed cart queues, address/fulfilment selection, payment discovery, payload, polling, and retry references.
</Card>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.