Skip to content
advanced Phase 115 · Headless

Headless Checkout in Magento 2

Cart API, checkout flow, payment integration, and session handling for headless Magento

45m
2 problems
Topic Progress 0%

Cart API Operations

GraphQL Cart Mutations

Create Cart

mutation CreateCart {
  createEmptyCart
}
# Returns: cart ID string

Add to Cart

mutation AddToCart($cartId: String!, $sku: String!, $qty: Int!) {
  addSimpleProductsToCart(
    input: {
      cart_id: $cartId
      cart_items: [
        { data: { quantity: $qty, sku: $sku } }
      ]
    }
  ) {
    cart {
      id
      total_quantity
      items {
        uid
        product {
          name
          sku
          price_range {
            minimum_price {
              regular_price { value currency }
            }
          }
        }
        quantity
      }
      prices {
        grand_total { value currency }
        subtotal_excluding_tax { value }
        discount_amounts { value }
      }
    }
  }
}

Update Cart Item

mutation UpdateCartItem($cartId: String!, $itemId: Int!, $qty: Int!) {
  updateSimpleProductsCart(
    cart_id: $cartId
    cart_items: [
      { cart_item_id: $itemId, quantity: $qty }
    ]
  ) {
    cart {
      items { uid quantity }
      prices { grand_total { value } }
    }
  }
}

Remove Cart Item

mutation RemoveFromCart($cartId: String!, $itemId: Int!) {
  removeItemFromCart(
    cart_id: $cartId
    cart_item_id: $itemId
  ) {
    cart {
      items { uid product { name } }
      total_quantity
    }
  }
}

Apply Coupon

mutation ApplyCoupon($cartId: String!, $couponCode: String!) {
  applyCouponToCart(
    cart_id: $cartId
    coupon_code: $couponCode
  ) {
    cart {
      prices {
        discount_amounts { value }
        grand_total { value }
      }
    }
  }
}

Client Implementation

// composables/useCart.ts
import { gql } from '@apollo/client';
import { useMutation } from '@vue/apollo-composable';

const ADD_TO_CART = gql`
  mutation AddToCart($cartId: String!, $sku: String!, $qty: Int!) {
    addSimpleProductsToCart(
      input: {
        cart_id: $cartId
        cart_items: [{ data: { quantity: $qty, sku: $sku } }]
      }
    ) {
      cart {
        id
        total_quantity
        items { uid product { name sku } quantity }
        prices { grand_total { value } }
      }
    }
  }
`;

export function useCart() {
  const { mutate: addToCart, loading, error } = useMutation(ADD_TO_CART);

  const addItem = async (cartId: string, sku: string, qty: number) => {
    const { data } = await addToCart({ cartId, sku, qty });
    return data.addSimpleProductsToCart.cart;
  };

  return { addItem, loading, error };
}

Checkout Flow

Checkout Steps

Step 1: Shipping Information

mutation SetShippingAddress(
  $cartId: String!
  $firstname: String!
  $lastname: String!
  $street: [String]!
  $city: String!
  $region: String!
  $postcode: String!
  $country: String!
  $telephone: String!
) {
  setShippingAddressesOnCart(
    input: {
      cart_id: $cartId
      shipping_addresses: [
        {
          address: {
            firstname: $firstname
            lastname: $lastname
            street: $street
            city: $city
            region: $region
            postcode: $postcode
            country_code: $country
            telephone: $telephone
          }
        }
      ]
    }
  ) {
    cart {
      shipping_addresses {
        firstname
        lastname
        available_shipping_methods {
          carrier_code
          method_code
          carrier_title
          method_title
          amount { value }
        }
      }
    }
  }
}

Step 2: Shipping Method

mutation SetShippingMethod(
  $cartId: String!
  $carrierCode: String!
  $methodCode: String!
) {
  setShippingMethodsOnCart(
    input: {
      cart_id: $cartId
      shipping_methods: [
        {
          carrier_code: $carrierCode
          method_code: $methodCode
        }
      ]
    }
  ) {
    cart {
      shipping_addresses {
        selected_shipping_method {
          carrier_code
          method_code
          amount { value }
        }
      }
      prices {
        grand_total { value }
      }
    }
  }
}

Step 3: Payment Method

mutation SetPaymentMethod(
  $cartId: String!
  $paymentMethod: String!
) {
  setPaymentMethodOnCart(
    input: {
      cart_id: $cartId
      payment_method: {
        code: $paymentMethod
      }
    }
  ) {
    cart {
      available_payment_methods {
        code
        title
      }
      selected_payment_method {
        code
        title
      }
    }
  }
}

Step 4: Place Order

mutation PlaceOrder($cartId: String!) {
  placeOrder(
    input: {
      cart_id: $cartId
    }
  ) {
    order {
      order_number
      order_id
    }
  }
}

Complete Checkout Flow

// composables/useCheckout.ts
export function useCheckout() {
  const { addItem } = useCart();
  
  const checkout = async (cartId: string, checkoutData: any) => {
    // 1. Set shipping address
    await setShippingAddress(cartId, checkoutData.shipping);
    
    // 2. Set shipping method
    await setShippingMethod(
      cartId,
      checkoutData.shippingMethod.carrierCode,
      checkoutData.shippingMethod.methodCode
    );
    
    // 3. Set billing address (if different)
    if (checkoutData.billingDifferent) {
      await setBillingAddress(cartId, checkoutData.billing);
    }
    
    // 4. Set payment method
    await setPaymentMethod(cartId, checkoutData.paymentMethod);
    
    // 5. Place order
    const order = await placeOrder(cartId);
    
    return order;
  };

  return { checkout };
}

Payment Integration

Payment Gateway Integration

Stripe Integration

// lib/stripe.ts
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

export async function createPaymentIntent(
  amount: number,
  currency: string
) {
  return await stripe.paymentIntents.create({
    amount: Math.round(amount * 100), // Convert to cents
    currency,
    automatic_payment_methods: { enabled: true }
  });
}

// Checkout component
export default function StripeCheckout() {
  const stripe = useStripe();
  const elements = useElements();

  const handleSubmit = async () => {
    const { error } = await stripe.confirmPayment({
      elements,
      confirmParams: {
        return_url: `${window.location.origin}/order-confirmation`
      }
    });

    if (error) {
      console.error(error.message);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <PaymentElement />
      <button type="submit">Pay</button>
    </form>
  );
}

PayPal Integration

// components/PayPalCheckout.tsx
import { PayPalScriptProvider, PayPalButtons } from '@paypal/react-paypal-js';

export default function PayPalCheckout({ amount, onSuccess }) {
  return (
    <PayPalScriptProvider options={{
      'client-id': process.env.PAYPAL_CLIENT_ID
    }}>
      <PayPalButtons
        createOrder={(data, actions) => {
          return actions.order.create({
            purchase_units: [{
              amount: { value: amount.toString() }
            }]
          });
        }}
        onApprove={(data, actions) => {
          return actions.order.capture().then((details) => {
            onSuccess(details.id);
          });
        }}
      />
    </PayPalScriptProvider>
  );
}

Custom Payment Handler

// lib/payment-handler.ts
export class PaymentHandler {
  private processors: Map<string, PaymentProcessor>;

  constructor() {
    this.processors = new Map();
    this.processors.set('stripe', new StripeProcessor());
    this.processors.set('paypal', new PayPalProcessor());
    this.processors.set('checkmo', new CheckmoProcessor());
  }

  async process(
    method: string,
    cartId: string,
    paymentData: any
  ): Promise<PaymentResult> {
    const processor = this.processors.get(method);
    if (!processor) {
      throw new Error(`Unknown payment method: ${method}`);
    }

    return processor.process(cartId, paymentData);
  }
}

Webhook Handling

// pages/api/webhooks/stripe.ts
import { NextApiRequest, NextApiResponse } from 'next';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const sig = req.headers['stripe-signature'];
  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  switch (event.type) {
    case 'payment_intent.succeeded':
      const paymentIntent = event.data.object;
      await handlePaymentSuccess(paymentIntent);
      break;
    case 'payment_intent.payment_failed':
      await handlePaymentFailure(event.data.object);
      break;
  }

  res.status(200).json({ received: true });
}

Session Handling

Session Management

Persistent Cart

// Store cart ID in localStorage
export function getCartId(): string | null {
  return localStorage.getItem('cart_id');
}

export function setCartId(cartId: string): void {
  localStorage.setItem('cart_id', cartId);
}

// Restore cart on page load
export async function restoreCart(): Promise<string> {
  let cartId = getCartId();
  
  if (cartId) {
    // Verify cart still exists
    try {
      const cart = await getCart(cartId);
      return cartId;
    } catch {
      // Cart expired or invalid
      localStorage.removeItem('cart_id');
    }
  }
  
  // Create new cart
  const newCartId = await createCart();
  setCartId(newCartId);
  return newCartId;
}

Guest vs Customer Cart

// Merge guest cart with customer cart on login
export async function mergeGuestCart(customerCartId: string) {
  const guestCartId = getCartId();
  
  if (!guestCartId) return;
  
  // Get guest cart items
  const guestCart = await getCart(guestCartId);
  
  // Add each item to customer cart
  for (const item of guestCart.items) {
    await addItemToCart(customerCartId, item.sku, item.quantity);
  }
  
  // Clear guest cart
  localStorage.removeItem('cart_id');
}

Session Timeout

// Auto-expire cart after inactivity
const CART_TIMEOUT = 30 * 60 * 1000; // 30 minutes

let cartTimer: NodeJS.Timeout;

export function resetCartTimer() {
  clearTimeout(cartTimer);
  cartTimer = setTimeout(() => {
    // Clear cart on timeout
    localStorage.removeItem('cart_id');
    // Optionally notify user
  }, CART_TIMEOUT);
}

// Reset on user activity
['click', 'keydown', 'scroll'].forEach(event => {
  document.addEventListener(event, resetCartTimer);
});

Cross-Tab Synchronization

// Sync cart across browser tabs
window.addEventListener('storage', (event) => {
  if (event.key === 'cart_id') {
    // Cart changed in another tab
    if (event.newValue) {
      // Refresh cart display
      refreshCart();
    } else {
      // Cart cleared
      redirectToHome();
    }
  }
});

Server-Side Session

// For SSR, use cookies
import { serialize, parse } from 'cookie';

// Set cart cookie
export function setCartCookie(res, cartId) {
  res.setHeader('Set-Cookie', serialize('cart_id', cartId, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    path: '/',
    maxAge: 60 * 60 * 24 * 7 // 1 week
  }));
}

// Get cart from cookie
export function getCartFromCookie(req) {
  const cookies = parse(req.headers.cookie || '');
  return cookies.cart_id;
}

Practice Problems

0 / 2 solved
Headless Checkout Flow

Implement a complete headless checkout flow with shipping, payment, and order placement.

Payment Integration

Integrate Stripe payment gateway with a headless Magento checkout.

Quiz

1. What GraphQL mutation creates an empty cart?

Question 1 options

2. How to persist cart across page refreshes?

Question 2 options

3. What is the checkout flow order?

Question 3 options

4. How to merge guest and customer carts?

Question 4 options

Flashcards

Question

What creates an empty cart?

Answer

createEmptyCart GraphQL mutation

Question

How to persist cart?

Answer

Store cart ID in localStorage or httpOnly cookie

Question

Checkout flow order?

Answer

Shipping → Payment → Place Order

Question

How to merge carts?

Answer

Add guest items to customer cart on login

Question

What handles payment webhooks?

Answer

API route that verifies signature and processes event

Revision Notes

Key Takeaways

  • 1. Cart operations: createEmptyCart, addSimpleProductsToCart, updateSimpleProductsCart, removeItemFromCart
  • 2. Checkout flow: shipping address → shipping method → payment method → place order
  • 3. Payment: integrate via client-side SDK (Stripe) or redirect (PayPal)
  • 4. Persist cart in localStorage (client) or cookie (SSR)
  • 5. Merge guest cart with customer cart on login
  • 6. Handle payment webhooks for async confirmation

Interview Tips

  • Walk through the headless checkout flow
  • How do you handle payment in a headless app?
  • How do you persist cart across sessions?
  • What happens to guest cart when customer logs in?
  • How do you handle payment webhooks?

Cheat Sheet

Headless Checkout Cheat Sheet

Cart Mutations:

  • createEmptyCart
  • addSimpleProductsToCart
  • updateSimpleProductsCart
  • removeItemFromCart
  • applyCouponToCart

Checkout Steps:

  1. setShippingAddressesOnCart
  2. setShippingMethodsOnCart
  3. setPaymentMethodOnCart
  4. placeOrder

Session:

  • localStorage: cart ID
  • Cookie: httpOnly for SSR
  • Merge on login

Payment:

  • Client-side SDK (Stripe)
  • Redirect (PayPal)
  • Webhooks for confirmation