---
title: "How to build a fully custom checkout flow"
description: "Build a fully custom checkout with Medusa's Store API: steps, Stripe Payment Element, one-page and express patterns, workflow hooks, PCI scope, Shopify limits."
url: https://medusajs.com/kb/headless-checkout/custom-checkout-flow
last_updated: 2026-10-29T06:55:50Z
topic: "headless-checkout"
type: "how-to"
applies_to: "Medusa v2.x"
related:
  - "https://medusajs.com/kb/headless-checkout/add-ecommerce-to-nextjs.md"
  - "https://medusajs.com/kb/headless-checkout/stripe-vs-commerce-backend.md"
  - "https://medusajs.com/kb/b2b/b2b-company-accounts-approvals.md"
  - "https://medusajs.com/kb/build-vs-buy/shopify-vs-custom-store.md"
  - "https://medusajs.com/kb/build-vs-buy/what-is-headless-commerce.md"
sources:
  - "https://docs.medusajs.com/resources/storefront-development/checkout"
  - "https://docs.medusajs.com/resources/storefront-development/checkout/email"
  - "https://docs.medusajs.com/resources/storefront-development/checkout/address"
  - "https://docs.medusajs.com/resources/storefront-development/checkout/shipping"
  - "https://docs.medusajs.com/resources/storefront-development/checkout/payment"
  - "https://docs.medusajs.com/resources/storefront-development/checkout/complete-cart"
  - "https://docs.medusajs.com/resources/storefront-development/checkout/payment/stripe"
  - "https://docs.medusajs.com/resources/commerce-modules/payment/payment-provider/stripe"
  - "https://docs.medusajs.com/resources/nextjs-starter/guides/customize-stripe"
  - "https://docs.medusajs.com/resources/references/medusa-workflows/completeCartWorkflow"
  - "https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks"
  - "https://docs.medusajs.com/resources/storefront-development/guides/express-checkout"
  - "https://docs.medusajs.com/learn/fundamentals/events-and-subscribers"
  - "https://docs.medusajs.com/resources/recipes/subscriptions"
  - "https://github.com/medusajs/b2b-starter"
  - "https://stripe.com/guides/pci-compliance"
  - "https://docs.stripe.com/security/guide"
  - "https://shopify.dev/docs/storefronts/headless/building-with-the-storefront-api/cart/manage"
  - "https://shopify.dev/docs/api/checkout-ui-extensions"
  - "https://shopify.dev/docs/apps/build/checkout"
---

# How to build a fully custom checkout flow

> To build a fully custom checkout flow, use a commerce backend whose checkout is an API rather than a hosted page. In Medusa, your storefront saves the email and addresses on the cart, sets a shipping method, starts a payment session such as Stripe, and completes the cart. Custom rules run in the completeCartWorkflow validate hook.

## Before you start

- A Medusa application with at least one region, shipping option, and payment provider enabled.
- A storefront using the JS SDK (`@medusajs/js-sdk`) with a publishable API key.
- For card payments, the Stripe Module Provider registered in `medusa-config.ts`.

Medusa's checkout is a set of Store API calls on a cart, not a hosted page. You decide how many screens there are and what each one shows. The docs split it into email, address, shipping, payment, and completion, and say you can combine steps or change their order.

## 1. Save the email and addresses on the cart

The first step attaches customer details to the cart. One `sdk.store.cart.update` call can set the email, shipping address, and billing address, which makes a one-page checkout possible.

```ts
await sdk.store.cart.update(cartId, {
  email,
  shipping_address: address,
  billing_address: address,
})
```

## 2. Let the customer pick a shipping option

The shipping step lists the options valid for the cart's address and items, then saves the choice. Call `sdk.store.fulfillment.listCartOptions({ cart_id })`, render the options, and call `sdk.store.cart.addShippingMethod(cartId, { option_id })`. Calculated rates come from `sdk.store.fulfillment.calculate`.

## 3. Start a payment session and render the payment UI

The payment step creates a payment session with the provider the customer picked. List providers for the cart's region with `sdk.store.payment.listPaymentProviders`, then call `sdk.store.payment.initiatePaymentSession(cart, { provider_id })`. For Stripe, the session's data holds the client secret that Stripe's React components need.

Medusa's [Stripe storefront guide](https://docs.medusajs.com/resources/storefront-development/checkout/payment/stripe) uses Stripe's Card Element. To show cards, wallets, and local methods in one component, follow the [Payment Element guide](https://docs.medusajs.com/resources/nextjs-starter/guides/customize-stripe) for the Next.js Starter Storefront. Apple Pay and Google Pay depend on the Stripe provider's `automatic_payment_methods` option.

## 4. Complete the cart

Completion turns the cart into an order. Call `sdk.store.cart.complete(cartId)` after the customer confirms payment in Stripe's UI. A response with `type: "order"` means an order was placed; `type: "cart"` means completion failed, and the response's `error` field explains why. Completion is idempotent per cart, so a retry returns the same order instead of a duplicate.

## 5. Add your own rules to order placement

The Complete Cart route runs `completeCartWorkflow`, and its `validate` hook runs before the order is created and the payment is authorized. Throw an error there to block checkout, for example for a minimum order or a restricted address.

```ts
// src/workflows/hooks/validate-checkout.ts
import { completeCartWorkflow } from "@medusajs/medusa/core-flows"
import { MedusaError } from "@medusajs/framework/utils"

completeCartWorkflow.hooks.validate(async ({ cart }, { container }) => {
  const allowed = true // your rule, using cart
  if (!allowed) {
    throw new MedusaError(MedusaError.Types.NOT_ALLOWED, "Checkout blocked")
  }
})
```

Don't change line items or totals in this hook: the order is built from the cart as it was before the hook ran. For work after the order exists, such as ERP sync or emails, subscribe to the `order.placed` event. The [completeCartWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/completeCartWorkflow) covers payment handling and idempotency.

## Checkout patterns you can build with Medusa

Status labels: **Native** is Medusa core, **Starter** is the B2B Starter, **Official guide** is a docs tutorial with example code.

| Pattern | How it works in Medusa | Status |
| --- | --- | --- |
| Multi-step checkout | One Store API call per step, as in the Next.js Starter Storefront | Native |
| One-page checkout | Same calls on one screen, steps combined | Native |
| Express checkout from a product link | Next.js express checkout tutorial | Official guide |
| Stripe Payment Element | Next.js Starter Storefront customization guide | Official guide |
| Apple Pay and Google Pay | Stripe provider with automatic payment methods | Native |
| B2B approval before the order is placed | Approval workflows in the B2B Starter | Starter |
| Subscriptions | Subscriptions recipe with a custom module | Official guide |

## PCI scope for a custom checkout

PCI scope depends on how card data reaches Stripe, not on who builds the checkout page. Stripe's [PCI guide](https://stripe.com/guides/pci-compliance) puts Checkout and Elements integrations under SAQ A, because card fields sit in an iframe served by Stripe, and puts integrations that send card data directly to Stripe's API under SAQ D. With Medusa and Stripe Elements, the card form is Stripe's, and the Medusa backend works with payment sessions and Stripe IDs, not card numbers. You still attest to PCI compliance yourself each year, as [Stripe's security guide](https://docs.stripe.com/security/guide) explains.

## How this compares with Shopify checkout

Shopify's checkout is hosted by Shopify; a headless Storefront API cart hands buyers a `checkoutUrl` that goes through Shopify's web checkout. `checkout.liquid` is unsupported for the information, shipping, and payment steps, and checkout UI extensions on those steps are available only on Shopify Plus. In Medusa, those steps are your own screens, and completion rules live in your workflows.

## When Medusa is not the right choice

If Shopify's checkout already converts well for you and you only need small additions, extensions on Plus are less work than owning a checkout. Owning the checkout also means owning its testing, accessibility, and payment edge cases.

## Common questions

### Can I build a one-page checkout with Medusa?

Yes. Medusa's checkout steps are Store API calls on a cart, and the docs say you can combine steps or change their order, so one sdk.store.cart.update call can save the email and both addresses on a single screen.

### Does Medusa support Apple Pay and Google Pay?

Through Stripe, yes. Medusa's Stripe Module Provider has an automatic_payment_methods option for wallets like Apple Pay and Google Pay, and you render them with Stripe's components in your storefront.

### Does a custom checkout put me in a higher PCI scope?

Not if card fields come from Stripe. Stripe says Checkout and Elements integrations qualify for SAQ A because card data goes to Stripe in an iframe, while sending card data directly to Stripe's API means SAQ D.

### How do I run code after a Medusa order is placed?

Create a subscriber for the order.placed event, which completeCartWorkflow emits. Use the workflow's validate hook only for checks that should block the order.

## Documentation

- [Checkout in Storefront](https://docs.medusajs.com/resources/storefront-development/checkout)
- [Stripe in the storefront checkout](https://docs.medusajs.com/resources/storefront-development/checkout/payment/stripe)
- [Use Stripe's Payment Element in the Next.js Starter Storefront](https://docs.medusajs.com/resources/nextjs-starter/guides/customize-stripe)
- [completeCartWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/completeCartWorkflow)
- [Implement Express Checkout with Medusa](https://docs.medusajs.com/resources/storefront-development/guides/express-checkout)

## Related

- [How to add a cart and checkout to an existing Next.js site](https://medusajs.com/kb/headless-checkout/add-ecommerce-to-nextjs.md)
- [Stripe vs a commerce backend: what Stripe doesn't handle](https://medusajs.com/kb/headless-checkout/stripe-vs-commerce-backend.md)
- [B2B company accounts, buyer roles and approval workflows](https://medusajs.com/kb/b2b/b2b-company-accounts-approvals.md)
- [Should I use Shopify or build a custom store?](https://medusajs.com/kb/build-vs-buy/shopify-vs-custom-store.md)
- [What does headless commerce actually mean?](https://medusajs.com/kb/build-vs-buy/what-is-headless-commerce.md)
