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.
Last modified: April 3, 2026
United States / English
Slovensko / Slovenčina
Canada / Français
Türkiye / Türkçe