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

# Build a Complete Checkout

> End-to-end tutorial: cart to payment in 30 minutes

# Build a Complete Checkout

This tutorial walks through building a complete checkout flow from scratch. Copy-paste ready, with all gotchas inline.

## Prerequisites

* Khaime API key (`pk_live_xxx` or `pk_sandbox_xxx`)
* Next.js/React project (or adapt to your framework)

***

## Step 1: Environment Setup

```bash theme={null}
# .env.local
NEXT_PUBLIC_KHAIME_API_URL=https://api.khaime.com/api/v1
NEXT_PUBLIC_KHAIME_API_KEY=pk_sandbox_your_key_here
NEXT_PUBLIC_KHAIME_SDK_URL=https://js.khaime.com/v1/
```

***

## Step 2: API Client

Create a reusable API client with proper error handling.

```typescript theme={null}
// lib/khaime-api.ts

const API_URL = process.env.NEXT_PUBLIC_KHAIME_API_URL;
const API_KEY = process.env.NEXT_PUBLIC_KHAIME_API_KEY;

const headers = {
  'Content-Type': 'application/json',
  'X-API-Key': API_KEY,
};

export interface CartItem {
  product_id: number;
  quantity: number;
  price: number;
  has_variation: boolean;
  product_image: string;
  product_variant_data: string;
  least_sub_variant_id?: string;
  main_variant?: string;
}

export interface DeliveryDetails {
  save_for_future_checkout: boolean;
  store_pick_up: boolean;
  national_delivery: boolean;
  international_delivery: boolean;
  address: Array<{
    name: string;
    email: string;
    address: string;
    city: string;
    location?: string;
    state: string;
    zip_code: string;
    country: string;
    country_code: string;
  }>;
}

/**
 * Validates cart and calculates shipping
 * Returns: cart_unique_id, shipping_fee, available_destinations
 */
export async function validateCart(
  cart: CartItem[],
  currency: string,
  deliveryDetails: DeliveryDetails
) {
  const response = await fetch(`${API_URL}/cart/validate`, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      cart,
      currency,
      delivery_details: deliveryDetails,
    }),
  });

  const data = await response.json();

  if (!data.status) {
    throw new Error(data.message || 'Cart validation failed');
  }

  return data;
}

/**
 * Creates payment intent
 * Returns: secret, publishable_key, amount_to_pay
 */
export async function createPaymentIntent(payload: {
  cart_unique_id: string;
  cart: CartItem[];
  email: string;
  first_name: string;
  last_name: string;
  currency: string;
  product_title: string;
  product_type: string;
  order_total_charge: number;
  delivery_details: DeliveryDetails;
}) {
  const response = await fetch(`${API_URL}/payment/intent`, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      ...payload,
      payment_type: 'one',
      payment_by: 'customer',
      is_coupon_used: false,
      is_second_time_payment: false,
    }),
  });

  const data = await response.json();

  if (!data.status) {
    throw new Error(data.message || 'Payment intent creation failed');
  }

  return data;
}
```

***

## Step 3: Build Cart Items Correctly

This is where most errors happen. The fields differ based on `has_variation`.

```typescript theme={null}
// utils/cart-helpers.ts

interface CartProduct {
  id: number;
  title: string;
  price: number;
  has_variation: boolean;
  images: string[];
}

interface CartVariant {
  id: string;
  name: string;
  price: number;
}

interface CartEntry {
  product: CartProduct;
  variant?: CartVariant;
  quantity: number;
}

/**
 * Builds cart items for API
 * CRITICAL: Different fields for variant vs non-variant products
 */
export function buildCartItems(entries: CartEntry[]): CartItem[] {
  return entries.map((entry) => {
    const hasVariation = entry.product.has_variation;

    // Base fields for ALL products
    const item: CartItem = {
      product_id: entry.product.id,  // Must be number
      quantity: entry.quantity,
      price: entry.variant?.price ?? entry.product.price,
      has_variation: hasVariation,
      product_image: entry.product.images?.[0] || '',
      // CRITICAL: Use 'none' for non-variant products
      product_variant_data: hasVariation && entry.variant
        ? entry.variant.name
        : 'none',
    };

    // ONLY add variant fields if has_variation is true
    if (hasVariation && entry.variant) {
      item.least_sub_variant_id = entry.variant.id;
      item.main_variant = entry.variant.name;
    }

    return item;
  });
}
```

***

## Step 4: Price Formatting (Multicurrency Safe)

```typescript theme={null}
// contexts/CurrencyContext.tsx

import { createContext, useContext, useState, useCallback } from 'react';

interface CurrencyContextType {
  currency: string;
  exchangeRate: number;
  formatPrice: (cents: number, skipConversion?: boolean) => string;
}

const CurrencyContext = createContext<CurrencyContextType | null>(null);

export function CurrencyProvider({ children }) {
  const [currency, setCurrency] = useState('USD');
  const [exchangeRate, setExchangeRate] = useState(1);
  const baseCurrency = 'USD';

  /**
   * Formats price in cents to display string
   *
   * @param cents - Amount in cents
   * @param skipConversion - TRUE for values already converted by API
   *                         (shipping_fee, amount_to_pay, order totals)
   */
  const formatPrice = useCallback((cents: number, skipConversion = false): string => {
    // CRITICAL: Some API values are ALREADY converted
    // Do NOT apply exchange rate to: shipping_fee, total_sum_with_shipping_fee, amount_to_pay
    const amount = (currency === baseCurrency || skipConversion)
      ? cents
      : Math.round(cents * exchangeRate);

    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: currency,
      minimumFractionDigits: 2,
    }).format(amount / 100);
  }, [currency, exchangeRate, baseCurrency]);

  return (
    <CurrencyContext.Provider value={{ currency, exchangeRate, formatPrice }}>
      {children}
    </CurrencyContext.Provider>
  );
}

export const useCurrency = () => {
  const context = useContext(CurrencyContext);
  if (!context) throw new Error('useCurrency must be used within CurrencyProvider');
  return context;
};
```

***

## Step 5: Complete Checkout Page

```tsx theme={null}
// app/checkout/page.tsx
'use client';

import { useState, useEffect, useRef } from 'react';
import { useRouter } from 'next/navigation';
import Script from 'next/script';
import { useCart } from '@/contexts/CartContext';
import { useCurrency } from '@/contexts/CurrencyContext';
import { validateCart, createPaymentIntent, CartItem, DeliveryDetails } from '@/lib/khaime-api';
import { buildCartItems } from '@/utils/cart-helpers';

// Declare Khaime SDK types
declare global {
  interface Window {
    Khaime?: {
      confirmPayment: (options: {
        secret: string;
        publishable_key: string;
        stripe_account_id?: string;
        amount?: number;
        currency?: string;
        display?: 'modal' | 'inline';
        container?: HTMLElement;
        onReady?: () => void;
        onSuccess?: (result: unknown) => void;
        onError?: (error: Error) => void;
      }) => Promise<{ error?: { message: string } }>;
    };
  }
}

// Shipping destination from API
interface ShippingDestination {
  rule_id: number;
  rule_name: string;
  destination_type: 'nationwide' | 'specific_states' | 'specific_locations' | 'international';
  destination_locations?: string[];
  shipping_fee?: number;
}

export default function CheckoutPage() {
  const router = useRouter();
  const { items, subtotal, clearCart } = useCart();
  const { formatPrice, currency } = useCurrency();

  // Flow state
  const [step, setStep] = useState<'shipping' | 'shipping-options' | 'payment'>('shipping');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // API response data
  const [cartUniqueId, setCartUniqueId] = useState<string | null>(null);
  const [shippingFee, setShippingFee] = useState(0);
  const [orderTotal, setOrderTotal] = useState<number | null>(null);
  const [shippingOptions, setShippingOptions] = useState<ShippingDestination[]>([]);
  const [selectedShippingRule, setSelectedShippingRule] = useState<number | null>(null);

  // Payment state
  const [showPaymentForm, setShowPaymentForm] = useState(false);
  const [paymentCredentials, setPaymentCredentials] = useState<{
    secret: string;
    publishable_key: string;
    stripe_account_id?: string;
    amount: number;
    currency: string;
  } | null>(null);
  const paymentContainerRef = useRef<HTMLDivElement>(null);

  // Shipping form
  const [shippingInfo, setShippingInfo] = useState({
    email: '',
    firstName: '',
    lastName: '',
    address: '',
    city: '',
    state: '',
    postalCode: '',
    country: 'US',
    location: '',  // For specific_locations rules
  });

  // ============================================================
  // STEP 1: Submit shipping address, validate cart, get shipping
  // ============================================================
  const handleShippingSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setError(null);

    try {
      const cartItems = buildCartItems(items);

      const deliveryDetails: DeliveryDetails = {
        save_for_future_checkout: false,
        store_pick_up: false,
        national_delivery: shippingInfo.country === 'US',
        international_delivery: shippingInfo.country !== 'US',
        address: [{
          name: `${shippingInfo.firstName} ${shippingInfo.lastName}`,
          email: shippingInfo.email.toLowerCase(),
          address: shippingInfo.address,
          city: shippingInfo.city,
          location: shippingInfo.location || undefined,
          state: shippingInfo.state,
          zip_code: shippingInfo.postalCode,
          country: shippingInfo.country,
          country_code: shippingInfo.country,
        }],
      };

      const response = await validateCart(cartItems, currency, deliveryDetails);
      const data = response.data;

      // Store cart ID for payment
      setCartUniqueId(data.cart_unique_id);

      // Extract shipping fee and total - ALREADY CONVERTED, don't convert again!
      setShippingFee(data.shipping_fee || 0);
      setOrderTotal(data.total_sum_with_shipping_fee);

      // Check for shipping options
      const shippingDetails = data.pricing_summary?.shipping_details;
      const availableDestinations = shippingDetails?.available_destinations;

      if (availableDestinations && availableDestinations.length > 1) {
        // Multiple options - enrich with fees from matched_rules
        const matchedRules = shippingDetails.matched_rules || [];
        const feeMap = new Map(matchedRules.map(r => [r.rule_id, r.calculated_fee]));

        const enriched = availableDestinations.map(dest => ({
          ...dest,
          shipping_fee: feeMap.get(dest.rule_id),
        }));

        setShippingOptions(enriched);
        setSelectedShippingRule(matchedRules[0]?.rule_id || enriched[0].rule_id);
        setStep('shipping-options');  // Show shipping selector
      } else {
        setStep('payment');  // Go directly to payment
      }
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Validation failed');
    } finally {
      setLoading(false);
    }
  };

  // ============================================================
  // STEP 2: Handle shipping option selection
  // ============================================================
  const handleShippingOptionSelect = (option: ShippingDestination) => {
    setSelectedShippingRule(option.rule_id);

    // Update shipping fee from selected option
    if (option.shipping_fee !== undefined) {
      setShippingFee(option.shipping_fee);
      // Recalculate total (both values already converted)
      const cartSubtotal = orderTotal ? orderTotal - shippingFee : subtotal;
      setOrderTotal(cartSubtotal + option.shipping_fee);
    }

    // For specific_locations, store the location
    if (option.destination_type === 'specific_locations' && option.destination_locations?.[0]) {
      setShippingInfo(prev => ({ ...prev, location: option.destination_locations![0] }));
    }
  };

  const handleConfirmShipping = () => {
    setStep('payment');
  };

  // ============================================================
  // STEP 3: Create payment intent
  // ============================================================
  const handlePayment = async () => {
    setLoading(true);
    setError(null);

    try {
      if (!cartUniqueId) {
        throw new Error('Cart not validated. Please go back and try again.');
      }

      const cartItems = buildCartItems(items);
      const firstProduct = items[0]?.product;

      const deliveryDetails: DeliveryDetails = {
        save_for_future_checkout: false,
        store_pick_up: false,
        national_delivery: shippingInfo.country === 'US',
        international_delivery: shippingInfo.country !== 'US',
        address: [{
          name: `${shippingInfo.firstName} ${shippingInfo.lastName}`,
          email: shippingInfo.email.toLowerCase(),
          address: shippingInfo.address,
          city: shippingInfo.city,
          location: shippingInfo.location || undefined,
          state: shippingInfo.state,
          zip_code: shippingInfo.postalCode,
          country: shippingInfo.country,
          country_code: shippingInfo.country,
        }],
      };

      const response = await createPaymentIntent({
        cart_unique_id: cartUniqueId,
        cart: cartItems,
        email: shippingInfo.email.toLowerCase(),
        first_name: shippingInfo.firstName,
        last_name: shippingInfo.lastName,
        currency,
        product_title: firstProduct?.title || 'Order',
        product_type: firstProduct?.product_type || 'physical_product',
        order_total_charge: subtotal,
        delivery_details: deliveryDetails,
        // Note: Shipping rule is determined by `location` field during cart validation
      });

      const { secret, publishable_key, amount_to_pay, currency: paymentCurrency, data } = response.data;

      // Update total from payment intent (source of truth)
      setOrderTotal(amount_to_pay);

      // Store credentials and show payment form
      setPaymentCredentials({
        secret,
        publishable_key,
        stripe_account_id: data?.stripe_account_id,
        amount: amount_to_pay,
        currency: paymentCurrency || currency,
      });
      setShowPaymentForm(true);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Payment failed');
    } finally {
      setLoading(false);
    }
  };

  // ============================================================
  // STEP 4: Initialize payment form when container is ready
  // ============================================================
  useEffect(() => {
    if (!showPaymentForm || !paymentCredentials) return;
    if (!paymentContainerRef.current) return;
    if (!window.Khaime?.confirmPayment) return;

    const initPayment = async () => {
      const result = await window.Khaime!.confirmPayment({
        secret: paymentCredentials.secret,
        publishable_key: paymentCredentials.publishable_key,
        stripe_account_id: paymentCredentials.stripe_account_id,
        amount: paymentCredentials.amount,
        currency: paymentCredentials.currency,
        display: 'inline',
        container: paymentContainerRef.current!,
        onSuccess: () => {
          clearCart();
          router.push('/checkout/success');
        },
        onError: (err) => {
          setError(err.message);
        },
      });

      if (result.error) {
        setError(result.error.message);
      }
    };

    initPayment();
  }, [showPaymentForm, paymentCredentials, clearCart, router]);

  // ============================================================
  // RENDER
  // ============================================================
  return (
    <>
      {/* Load Khaime SDK */}
      <Script
        src={process.env.NEXT_PUBLIC_KHAIME_SDK_URL}
        strategy="afterInteractive"
      />

      <div className="max-w-2xl mx-auto p-6">
        <h1 className="text-2xl font-bold mb-6">Checkout</h1>

        {error && (
          <div className="bg-red-50 border border-red-200 text-red-700 p-4 rounded mb-6">
            {error}
          </div>
        )}

        {/* STEP 1: Shipping Form */}
        {step === 'shipping' && (
          <form onSubmit={handleShippingSubmit} className="space-y-4">
            <h2 className="text-lg font-semibold">Shipping Address</h2>

            <input
              type="email"
              name="email"
              placeholder="Email"
              value={shippingInfo.email}
              onChange={(e) => setShippingInfo(prev => ({ ...prev, email: e.target.value }))}
              required
              className="w-full p-3 border rounded"
            />

            <div className="grid grid-cols-2 gap-4">
              <input
                type="text"
                placeholder="First Name"
                value={shippingInfo.firstName}
                onChange={(e) => setShippingInfo(prev => ({ ...prev, firstName: e.target.value }))}
                required
                className="p-3 border rounded"
              />
              <input
                type="text"
                placeholder="Last Name"
                value={shippingInfo.lastName}
                onChange={(e) => setShippingInfo(prev => ({ ...prev, lastName: e.target.value }))}
                required
                className="p-3 border rounded"
              />
            </div>

            <input
              type="text"
              placeholder="Address"
              value={shippingInfo.address}
              onChange={(e) => setShippingInfo(prev => ({ ...prev, address: e.target.value }))}
              required
              className="w-full p-3 border rounded"
            />

            <div className="grid grid-cols-3 gap-4">
              <input
                type="text"
                placeholder="City"
                value={shippingInfo.city}
                onChange={(e) => setShippingInfo(prev => ({ ...prev, city: e.target.value }))}
                required
                className="p-3 border rounded"
              />
              <input
                type="text"
                placeholder="State"
                value={shippingInfo.state}
                onChange={(e) => setShippingInfo(prev => ({ ...prev, state: e.target.value }))}
                required
                className="p-3 border rounded"
              />
              <input
                type="text"
                placeholder="ZIP"
                value={shippingInfo.postalCode}
                onChange={(e) => setShippingInfo(prev => ({ ...prev, postalCode: e.target.value }))}
                required
                className="p-3 border rounded"
              />
            </div>

            <button
              type="submit"
              disabled={loading}
              className="w-full bg-black text-white py-3 rounded disabled:bg-gray-400"
            >
              {loading ? 'Calculating Shipping...' : 'Continue'}
            </button>
          </form>
        )}

        {/* STEP 2: Shipping Options (if multiple) */}
        {step === 'shipping-options' && (
          <div className="space-y-4">
            <h2 className="text-lg font-semibold">Select Shipping Method</h2>

            {shippingOptions.map((option) => (
              <label
                key={option.rule_id}
                className={`flex justify-between p-4 border rounded cursor-pointer ${
                  selectedShippingRule === option.rule_id ? 'border-black bg-gray-50' : ''
                }`}
                onClick={() => handleShippingOptionSelect(option)}
              >
                <div>
                  <input
                    type="radio"
                    checked={selectedShippingRule === option.rule_id}
                    onChange={() => {}}
                    className="mr-3"
                  />
                  <span className="font-medium">{option.rule_name}</span>
                  <span className="text-gray-500 ml-2">
                    ({option.destination_type.replace(/_/g, ' ')})
                  </span>
                </div>
                {option.shipping_fee !== undefined && (
                  <span className="font-semibold">
                    {formatPrice(option.shipping_fee, true)}
                  </span>
                )}
              </label>
            ))}

            <button
              onClick={handleConfirmShipping}
              className="w-full bg-black text-white py-3 rounded"
            >
              Continue to Payment
            </button>
          </div>
        )}

        {/* STEP 3: Payment */}
        {step === 'payment' && (
          <div className="space-y-4">
            <h2 className="text-lg font-semibold">Payment</h2>

            {/* Order Summary */}
            <div className="bg-gray-50 p-4 rounded space-y-2">
              <div className="flex justify-between">
                <span>Subtotal</span>
                <span>{formatPrice(subtotal)}</span>
              </div>
              <div className="flex justify-between">
                <span>Shipping</span>
                {/* CRITICAL: skipConversion=true for shipping */}
                <span>{formatPrice(shippingFee, true)}</span>
              </div>
              <div className="flex justify-between font-bold border-t pt-2">
                <span>Total</span>
                {/* CRITICAL: skipConversion=true for total */}
                <span>{formatPrice(orderTotal || subtotal, true)}</span>
              </div>
            </div>

            {!showPaymentForm ? (
              <button
                onClick={handlePayment}
                disabled={loading}
                className="w-full bg-black text-white py-3 rounded disabled:bg-gray-400"
              >
                {loading ? 'Loading...' : `Pay ${formatPrice(orderTotal || subtotal, true)}`}
              </button>
            ) : (
              <div ref={paymentContainerRef} className="min-h-[200px]" />
            )}
          </div>
        )}
      </div>
    </>
  );
}
```

***

## Key Takeaways

<CardGroup cols={2}>
  <Card title="Cart Items" icon="cart-shopping">
    Use `'none'` for `product_variant_data` on non-variant products. Only include `main_variant` and `least_sub_variant_id` when `has_variation: true`.
  </Card>

  <Card title="Currency Conversion" icon="money-bill">
    `shipping_fee`, `total_sum_with_shipping_fee`, and `amount_to_pay` are ALREADY CONVERTED. Pass `skipConversion: true` to formatPrice.
  </Card>

  <Card title="Shipping Options" icon="truck">
    Check `available_destinations.length > 1` after cart validation. Show selector before payment if multiple options.
  </Card>

  <Card title="Payment Timing" icon="clock">
    Use `useEffect` to call `confirmPayment` AFTER the container renders. Pass the DOM element, not an ID string.
  </Card>
</CardGroup>

***

## Common Errors Reference

| Error                                     | Cause                        | Fix                                     |
| ----------------------------------------- | ---------------------------- | --------------------------------------- |
| `main_variant not allowed to be empty`    | Sent for non-variant product | Only include when `has_variation: true` |
| `product_variant_data is required`        | Missing field                | Use `'none'` for non-variant            |
| `Container element not found`             | Called before render         | Use `useEffect` with deps               |
| Prices doubled (e.g., $145,800 vs $8,100) | Double-converted shipping    | Use `skipConversion: true`              |
| `cart_unique_id is undefined`             | Skipped cart validation      | Always validate before payment intent   |
