The checkout page is where revenue happens or dies. In headless WooCommerce, you’re building it from scratch — no default WooCommerce checkout template, no pre-built payment form, no automatic shipping calculation. This is both the biggest challenge and the biggest opportunity of going headless.

Checkout Architecture

A headless checkout typically has 3-4 steps, all communicating with WooCommerce via API:

Cart Review → Shipping Info → Payment → Confirmation

Store API WC REST API Stripe/ WC REST API

(cart) (shipping) PayPal JS (create order)

Cart Management with WooCommerce Store API

The WooCommerce Store API (included in WooCommerce 6.0+) provides cart endpoints designed for headless use:

// Cart state management (Next.js)

const STORE_API = ${process.env.WP_URL}/wp-json/wc/store/v1;

export async function getCart(nonce) {

const res = await fetch(${STORE_API}/cart, {

credentials: 'include',

headers: { 'Nonce': nonce }

});

return res.json();

}

export async function addToCart(productId, quantity, nonce) {

const res = await fetch(${STORE_API}/cart/add-item, {

method: 'POST',

credentials: 'include',

headers: {

'Content-Type': 'application/json',

'Nonce': nonce

},

body: JSON.stringify({ id: productId, quantity })

});

return res.json();

}

export async function applyCoupon(code, nonce) {

const res = await fetch(${STORE_API}/cart/apply-coupon, {

method: 'POST',

credentials: 'include',

headers: {

'Content-Type': 'application/json',

'Nonce': nonce

},

body: JSON.stringify({ code })

});

return res.json();

}

Important: The Store API uses a nonce for cart session management. Fetch it from the wc/store/v1/cart response header Nonce.

Shipping Calculation

WooCommerce calculates shipping server-side based on the customer’s address:

export async function updateShippingAddress(address, nonce) {

const res = await fetch(${STORE_API}/cart/update-customer, {

method: 'POST',

credentials: 'include',

headers: {

'Content-Type': 'application/json',

'Nonce': nonce

},

body: JSON.stringify({

shipping_address: {

first_name: address.firstName,

last_name: address.lastName,

address_1: address.address1,

city: address.city,

state: address.state,

postcode: address.postcode,

country: address.country

}

})

});

const cart = await res.json();

// cart.shipping_rates contains available methods with costs

return cart;

}

The response includes calculated shipping rates that you display as options.

Payment Integration: Stripe

Stripe is the most common payment gateway for headless WooCommerce. Use Stripe Elements for PCI-compliant card collection:

import { loadStripe } from '@stripe/stripe-js';

import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js';

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PK);

function CheckoutForm({ cart }) {

const stripe = useStripe();

const elements = useElements();

const handleSubmit = async (e) => {

e.preventDefault();

// 1. Create payment intent on your server

const { clientSecret } = await fetch('/api/create-payment-intent', {

method: 'POST',

headers: { 'Content-Type': 'application/json' },

body: JSON.stringify({ amount: cart.totals.total_price })

}).then(r => r.json());

// 2. Confirm payment with Stripe

const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, {

payment_method: {

card: elements.getElement(CardElement),

billing_details: { name: cart.billing_address.first_name }

}

});

if (error) {

setError(error.message);

return;

}

// 3. Create WooCommerce order

if (paymentIntent.status === 'succeeded') {

await createWooCommerceOrder(cart, paymentIntent.id);

}

};

return (

);

}

Creating the WooCommerce Order

After successful payment, create the order via WooCommerce REST API:

async function createWooCommerceOrder(cart, paymentIntentId) {

const orderData = {

payment_method: 'stripe',

payment_method_title: 'Credit Card (Stripe)',

set_paid: true,

transaction_id: paymentIntentId,

billing: cart.billing_address,

shipping: cart.shipping_address,

line_items: cart.items.map(item => ({

product_id: item.id,

variation_id: item.variation?.[0]?.attribute || undefined,

quantity: item.quantity

})),

shipping_lines: [{

method_id: cart.shipping_rates[0]?.rate_id,

method_title: cart.shipping_rates[0]?.name,

total: cart.shipping_rates[0]?.price

}],

coupon_lines: cart.coupons.map(c => ({ code: c.code })),

meta_data: [

{ key: '_stripe_payment_intent', value: paymentIntentId }

]

};

const response = await fetch(${WP_URL}/wp-json/wc/v3/orders, {

method: 'POST',

headers: {

'Content-Type': 'application/json',

'Authorization': Basic ${btoa(${CK}:${CS})}

},

body: JSON.stringify(orderData)

});

return response.json();

}

Checkout UX Best Practices

  • Single-page checkout outperforms multi-step for most stores. Use accordion sections instead of separate pages.
  • Address autocomplete via Google Places API reduces friction and errors.
  • Real-time validation — validate fields on blur, not on submit.
  • Guest checkout by default — don’t force account creation before purchase.
  • Order summary always visible — on desktop, show cart summary in a sidebar.
  • Express checkout buttons — Apple Pay, Google Pay, and PayPal above the fold.

Error Handling

Checkout errors lose customers. Handle every failure gracefully:

const ERROR_MESSAGES = {

'card_declined': 'Your card was declined. Please try another card.',

'insufficient_funds': 'Insufficient funds. Please try another payment method.',

'expired_card': 'Your card has expired. Please update your card details.',

'processing_error': 'A processing error occurred. Please try again.',

'stock_error': 'Some items in your cart are no longer available.',

'shipping_error': 'We cannot ship to this address. Please check your details.'

};

Conclusion

The headless checkout is the hardest part of a headless WooCommerce build, but it’s also where you have the most control over the customer experience. Use the WooCommerce Store API for cart management, Stripe Elements for PCI-compliant payments, and focus relentlessly on reducing friction. Every extra field, every unnecessary page load, every confusing error message costs conversions. Build lean, test with real users, and iterate.

Leave a Reply

Your email address will not be published. Required fields are marked *

Close Search Window