> ## 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.

# Checkout Integration Guide

> Complete guide for implementing cart validation, shipping calculation, and payment processing

# Checkout Integration Guide

This guide covers the complete checkout flow for physical products, including cart validation, shipping calculation, and payment processing. Follow this guide to avoid common pitfalls.

## Critical Concepts

### Frontend Displays, Backend Calculates

<Error>
  **The frontend should NEVER calculate prices.** All pricing must come from the backend API.

  This includes:

  * Product subtotals
  * Shipping fees
  * Order totals
  * Currency conversions

  The frontend's job is to:

  1. Send cart items to the API
  2. Display the values the API returns
  3. Never do math on prices
</Error>

```typescript theme={null}
// WRONG - Never calculate prices on frontend
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const total = subtotal + shippingFee;
const convertedPrice = price * exchangeRate;

// RIGHT - Use values from API response
const { cart_subtotal, shipping_fee, cart_total } = await validateCart(items);
setSubtotal(cart_subtotal);
setShipping(shipping_fee);
setTotal(cart_total);
```

### All Amounts Are in Cents

**Every monetary value in the Khaime API is in cents (smallest currency unit).**

| Display Value | API Value (cents) |
| ------------- | ----------------- |
| \$4.50        | 450               |
| \$45.00       | 4500              |
| \$450.00      | 45000             |

```javascript theme={null}
// Converting for display
const displayPrice = apiAmountInCents / 100;

// Converting user input to API format
const apiAmount = userDollarAmount * 100;
```

<Warning>
  A common configuration error is entering shipping as `450` thinking it means $4.50, when the API interprets it as $4.50 (450 cents). If you meant \$450, you'd enter `45000`.
</Warning>

### Shipping is Calculated Server-Side

Shipping fees are calculated by the backend based on:

* Merchant's shipping rules configuration
* Delivery address (country, state, zip code)
* Cart contents (weight, dimensions, quantity)
* Shipping method selected

**You cannot calculate shipping client-side.** You must call the API with delivery details to get the actual shipping cost.

***

## Shipping Rules System

Merchants can configure multiple shipping rules that apply to different destinations. The API evaluates all rules and returns both the matched rule and available options.

### Shipping Rule Types

| Type                 | Description                                            | Example                              |
| -------------------- | ------------------------------------------------------ | ------------------------------------ |
| `nationwide`         | Any domestic destination within merchant's country     | US merchant shipping anywhere in US  |
| `specific_states`    | One or more configured states                          | Texas, California only               |
| `specific_locations` | Specific cities/areas within selected states           | Ikeja, Lekki within Lagos state      |
| `international`      | Outside merchant's country (specific countries or any) | Shipping to Canada, UK, or worldwide |

### Charge Types

| Charge Type          | Description                                           |
| -------------------- | ----------------------------------------------------- |
| `flat_rate`          | Fixed shipping cost regardless of order               |
| `order_amount_based` | Shipping varies by order total (e.g., free over \$50) |
| `product_based`      | Shipping varies by product quantity                   |
| `free_shipping`      | No shipping charge                                    |

<Tip>
  **Rule Priority**: The backend ranks rules in this order:

  1. `specific_locations` (most specific)
  2. `specific_states`
  3. `nationwide`
  4. General `international` fallback

  If a customer in Lagos, Ikeja has all rules available, they get the `specific_locations` Ikeja rate.
</Tip>

### API Response Structure

When validating a cart, the response includes shipping details in `pricing_summary`:

```typescript theme={null}
interface CartValidationResponse {
  status: boolean;
  data: {
    cart_unique_id: string;
    shipping_fee: number;                    // Matched rule's fee in CENTS
    total_sum_with_shipping_fee: number;     // Order total including shipping
    pricing_summary: {
      shipping_cost: number;                 // Same as shipping_fee
      shipping_details: {
        source: 'shipping_rules' | 'easypost' | 'uber_direct' | 'standard';
        can_switch_rates: boolean;
        matched_rules: MatchedShippingRule[];
        available_destinations: ShippingDestination[];
      };
    };
  };
}

// What matched the customer's address
interface MatchedShippingRule {
  rule_id: number;
  rule_name: string;
  destination_type: 'nationwide' | 'specific_states' | 'specific_locations' | 'international';
  charge_type: 'flat_rate' | 'order_amount_based' | 'product_based' | 'free_shipping';
  calculated_fee: number;  // Fee in CENTS
}

// All available shipping destinations (for UI selectors)
interface ShippingDestination {
  rule_id: number;
  rule_name: string;
  destination_type: 'nationwide' | 'specific_states' | 'specific_locations' | 'international';
  destination_country_code: string | null;  // null for nationwide
  destination_states: string[];             // e.g., ["Texas", "California"]
  destination_locations: string[];          // e.g., ["Ikeja", "Lekki"]
  destination_countries: Array<{ code: string; name: string }>;
  international_any_country: boolean;       // true = ships worldwide
}
```

### Example Response

```json theme={null}
{
  "status": true,
  "data": {
    "cart_unique_id": "generated-cart-id",
    "shipping_fee": 200000,
    "total_sum_with_shipping_fee": 1200000,
    "pricing_summary": {
      "shipping_cost": 200000,
      "shipping_details": {
        "source": "shipping_rules",
        "can_switch_rates": false,
        "matched_rules": [
          {
            "rule_id": 41,
            "rule_name": "Lagos locations",
            "destination_type": "specific_locations",
            "charge_type": "flat_rate",
            "calculated_fee": 200000
          }
        ],
        "available_destinations": [
          {
            "rule_id": 41,
            "rule_name": "Lagos locations",
            "destination_type": "specific_locations",
            "destination_country_code": "NG",
            "destination_states": ["Lagos"],
            "destination_locations": ["Ikeja", "Lekki"],
            "destination_countries": [],
            "international_any_country": false
          },
          {
            "rule_id": 42,
            "rule_name": "Nationwide Nigeria",
            "destination_type": "nationwide",
            "destination_country_code": null,
            "destination_states": [],
            "destination_locations": [],
            "destination_countries": [],
            "international_any_country": false
          }
        ]
      }
    }
  }
}
```

### The `location` Field

For `specific_locations` rules, the customer must select a merchant-defined location (city, area, or zone). This is separate from the `city` field:

| Field      | Purpose                                                         |
| ---------- | --------------------------------------------------------------- |
| `city`     | Customer's actual address city (for receipts, orders, couriers) |
| `location` | Merchant-defined area for shipping rule matching                |

Send the selected location in `delivery_details.address[0].location`:

```typescript theme={null}
const deliveryDetails = {
  address: [{
    name: "Customer Name",
    address: "123 Main St",
    city: "Lagos",           // Actual city for receipts
    location: "Ikeja",       // Merchant-defined area for rule matching
    state: "Lagos",
    country: "Nigeria",
    country_code: "NG",
    zip_code: "100001"
  }]
};
```

If `location` is omitted, the backend falls back to matching against `city`.

### Displaying Shipping Options

When `available_destinations` has multiple options, let users choose their shipping method:

```typescript theme={null}
const [selectedShippingRule, setSelectedShippingRule] = useState<number | null>(null);
const [shippingOptions, setShippingOptions] = useState<ShippingRule[]>([]);

// After cart validation
const handleShippingValidation = async () => {
  const response = await validateCart(cartItems, deliveryDetails);

  if (response.status) {
    const { shipping_details, shipping_fee } = response.data;

    // Store available options
    if (shipping_details?.available_destinations?.length > 1) {
      setShippingOptions(shipping_details.available_destinations);

      // Default to matched rule
      const matchedRuleId = shipping_details.matched_rules[0]?.rule_id;
      setSelectedShippingRule(matchedRuleId);
    }

    setShippingFee(shipping_fee);
  }
};

// UI for shipping options
{shippingOptions.length > 1 && (
  <div className="shipping-options">
    <h4>Select Shipping Method</h4>
    {shippingOptions.map((option) => (
      <label key={option.rule_id}>
        <input
          type="radio"
          name="shipping"
          value={option.rule_id}
          checked={selectedShippingRule === option.rule_id}
          onChange={() => {
            setSelectedShippingRule(option.rule_id);
            setShippingFee(option.shipping_fee);
          }}
        />
        <span>{option.rule_name}</span>
        <span>{formatCurrency(option.shipping_fee)}</span>
      </label>
    ))}
  </div>
)}
```

### How Shipping Selection Works

Shipping rules are selected during **cart validation**, not payment intent.

<Error>
  **Critical: Do NOT re-validate after shipping selection**

  The `cart_unique_id` returned from validation has the shipping fee cached. If you re-validate without the `location` field, you'll get a NEW `cart_unique_id` with the default shipping, overwriting the user's selection.
</Error>

**Correct Flow:**

```typescript theme={null}
// 1. Initial validation (shipping step) - returns available_destinations
const initialResponse = await validateCart(cartItems, currency, deliveryDetails);
const availableDestinations = initialResponse.data.pricing_summary.shipping_details.available_destinations;

// 2. User selects "Dallas Rate" - RE-VALIDATE with location
if (selectedOption.destination_type === 'specific_locations') {
  const selectedLocation = selectedOption.destination_locations[0];  // e.g., "Dallas"

  const revalidateResponse = await validateCart(cartItems, currency, {
    ...deliveryDetails,
    address: [{
      ...deliveryDetails.address[0],
      location: selectedLocation,  // CRITICAL: Include selected location
    }]
  });

  // Store this cart_unique_id - it has the correct shipping cached
  cartUniqueId = revalidateResponse.data.cart_unique_id;
  shippingFee = revalidateResponse.data.shipping_fee;  // Now reflects Dallas Rate
}

// 3. Payment intent - use the stored cart_unique_id, DO NOT re-validate
const paymentResponse = await createPaymentIntent({
  cart_unique_id: cartUniqueId,  // From step 2, not a new validation
  // ...
});
```

**Key Points:**

* The `location` field tells the backend which `specific_locations` rule to match
* The `cart_unique_id` has the shipping fee cached - don't overwrite it
* Payment intent uses the cached shipping from the `cart_unique_id`

### Rule Configuration Examples

| Rule Name                 | Type                 | Use Case                         |
| ------------------------- | -------------------- | -------------------------------- |
| "Standard US Shipping"    | `nationwide`         | Default for all US addresses     |
| "International Flat Rate" | `international`      | Default for non-US addresses     |
| "NYC Metro"               | `specific_city`      | Faster/cheaper for New York City |
| "West Coast Zone"         | `specific_locations` | Discounted rate for CA, OR, WA   |
| "Store Pickup - Dallas"   | `specific_locations` | Free pickup at Dallas location   |

<Warning>
  **Shipping Fee Display**: The `shipping_fee` at the top level of the response reflects the `matched_rules` fee. If the user selects a different option from `available_destinations`, update your UI to show that option's fee instead.
</Warning>

***

## Checkout Flow Overview

```
┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  1. Cart Page   │────▶│ 2. Shipping Form │────▶│ 3. Review/Pay   │────▶│  4. Success     │
│                 │     │                  │     │                 │     │                 │
│ - Show subtotal │     │ - Collect address│     │ - Show total    │     │ - Clear cart    │
│ - No shipping   │     │ - Validate cart  │     │ - Payment form  │     │ - Redirect      │
│   yet           │     │ - Get shipping   │     │ - Confirm pay   │     │                 │
└─────────────────┘     └──────────────────┘     └─────────────────┘     └─────────────────┘
```

***

## Step 1: Build Cart Items

Cart items must include specific fields. The requirements differ based on whether products have variations.

### Cart Item Structure

```typescript theme={null}
interface CartItem {
  product_id: number;           // Required: Product ID (must be number, not string)
  quantity: number;             // Required: Quantity
  price: number;                // Required: Price in CENTS
  has_variation: boolean;       // Required: Whether product has variants
  product_image: string;        // Required: First product image URL
  product_variant_data: string; // Required: Variant name or 'none'

  // Only include these if has_variation is true:
  least_sub_variant_id?: string; // Variant ID
  main_variant?: string;         // Variant name
}
```

### Building Cart Items Correctly

```typescript theme={null}
const cartItems = items.map((item) => {
  const hasVariation = item.product?.has_variation ?? false;

  // Base fields for ALL products
  const cartItem: Record<string, unknown> = {
    product_id: item.product.id,  // Must be number
    quantity: item.quantity,
    price: item.variant?.price ?? item.product.price,  // In cents
    has_variation: hasVariation,
    product_image: item.product.images?.[0] || '',
    product_variant_data: hasVariation && item.variant
      ? item.variant.name
      : 'none',  // Use 'none' for products without variations
  };

  // ONLY add variant fields if product HAS variations
  if (hasVariation && item.variant) {
    cartItem.least_sub_variant_id = item.variant.id;
    cartItem.main_variant = item.variant.name;
  }

  return cartItem;
});
```

<Warning>
  Common validation errors:

  * `'main_variant' is not allowed to be empty` - You included `main_variant` for a product without variations
  * `'product_variant_data' is required` - Missing this field (use `'none'` for non-variant products)
  * `'least_sub_variant_id' is not allowed` - You included variant ID for a non-variant product
</Warning>

***

## Step 2: Validate Cart with Shipping Address

**Call cart validation WHEN THE USER SUBMITS THEIR SHIPPING ADDRESS**, not when they click "Pay". This ensures they see the full total (including shipping) before entering payment details.

### Delivery Details Structure

```typescript theme={null}
const deliveryDetails = {
  save_for_future_checkout: false,
  store_pick_up: false,
  national_delivery: true,      // or international_delivery: true
  international_delivery: false,
  address: [
    {
      name: `${firstName} ${lastName}`,
      email: email.toLowerCase(),  // Must be lowercase
      address: streetAddress,
      city: city,
      state: state,
      zip_code: postalCode,
      country: countryName,        // e.g., "United States"
      country_code: countryCode,   // e.g., "US"
    },
  ],
};
```

### Validate Cart API Call

```typescript theme={null}
const response = await fetch(`${API_BASE}/cart/validate`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': API_KEY,
  },
  body: JSON.stringify({
    cart: cartItems,
    currency: 'USD',
    delivery_details: deliveryDetails,
  }),
});

const result = await response.json();
```

### Cart Validation Response

```typescript theme={null}
interface CartValidationResponse {
  status: boolean;
  message: string;
  data: {
    cart_unique_id: string;     // Save this for payment intent
    shipping_fee: number;       // Shipping cost in CENTS
    cart_total?: number;        // Total including shipping in CENTS
    // ... other fields
  };
}
```

### Extract Shipping and Update UI

```typescript theme={null}
if (result.status) {
  const { shipping_fee, cart_total } = result.data;

  // shipping_fee is in CENTS
  setShippingFee(shipping_fee);  // e.g., 450 = $4.50

  // Calculate or use provided total
  const total = cart_total || (subtotal + shipping_fee);
  setOrderTotal(total);

  // Now show the payment step with accurate totals
  setStep('payment');
}
```

***

## Step 3: Create Payment Intent

After cart validation, create a payment intent when the user is ready to pay.

### Payment Intent Request

```typescript theme={null}
const paymentPayload = {
  cart_unique_id: cartValidationResponse.data.cart_unique_id,
  cart: cartItems,
  email: shippingInfo.email.toLowerCase(),
  first_name: shippingInfo.firstName,
  last_name: shippingInfo.lastName,
  currency: 'USD',
  payment_type: 'one',           // 'one' for one-time, 'subscription' for recurring
  payment_by: 'customer',
  product_type: 'physical_product',  // or 'digital_product', 'service'
  product_title: productTitle,
  is_coupon_used: false,
  is_second_time_payment: false,
  order_total_charge: subtotal,  // Cart subtotal in CENTS (before shipping)
  delivery_details: deliveryDetails,
};

const response = await fetch(`${API_BASE}/payment/intent`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': API_KEY,
  },
  body: JSON.stringify(paymentPayload),
});
```

### Payment Intent Response

```typescript theme={null}
interface PaymentIntentResponse {
  status: boolean;
  message: string;
  data: {
    secret: string;              // Stripe client secret (pi_xxx_secret_yyy)
    publishable_key: string;     // Stripe publishable key
    amount_to_pay: number;       // FINAL amount in CENTS (includes shipping, fees)
    currency: string;
    shipping_fee?: number;       // Shipping in CENTS
    data?: {
      stripe_account_id?: string; // For Connect accounts
    };
  };
}
```

<Warning>
  **Amount Mismatch Warning**: The `amount_to_pay` from the API may differ from your frontend subtotal because it includes:

  * Shipping fees
  * Platform fees
  * Taxes (if applicable)
  * Discounts applied

  Always display `amount_to_pay` as the final total, not your calculated subtotal.
</Warning>

***

## Step 4: Render Payment Element

Use `Khaime.confirmPayment()` to render the Stripe Payment Element.

### Display Modes

| Mode     | Use Case              | Container Required |
| -------- | --------------------- | ------------------ |
| `modal`  | Overlay payment form  | No                 |
| `inline` | Embedded in your page | Yes                |

### Inline Mode (Recommended for Custom Checkout)

```typescript theme={null}
const result = await Khaime.confirmPayment({
  secret: paymentIntent.secret,
  publishable_key: paymentIntent.publishable_key,
  stripe_account_id: paymentIntent.data?.stripe_account_id,
  amount: paymentIntent.amount_to_pay,  // In CENTS
  currency: paymentIntent.currency,
  display: 'inline',
  container: containerElement,  // DOM element or ID string
  onReady: () => {
    console.log('Payment form ready');
  },
  onSuccess: (result) => {
    clearCart();
    router.push('/checkout/success');
  },
  onError: (error) => {
    setError(error.message);
  },
});
```

***

## React Integration: Critical Timing Issue

<Error>
  **The container element must exist in the DOM before calling `confirmPayment`.**

  React's asynchronous rendering means the container may not exist immediately after setting state.
</Error>

### Wrong Approach (Will Fail)

```typescript theme={null}
// DON'T DO THIS - container doesn't exist yet
const handlePayment = async () => {
  const paymentIntent = await createPaymentIntent();
  setShowPaymentForm(true);  // Triggers re-render

  // Container doesn't exist yet! React hasn't re-rendered.
  await Khaime.confirmPayment({
    container: 'payment-container',  // ERROR: Element not found
    // ...
  });
};
```

### Correct Approach (Use useEffect)

```typescript theme={null}
const [paymentCredentials, setPaymentCredentials] = useState(null);
const [showPaymentForm, setShowPaymentForm] = useState(false);
const paymentContainerRef = useRef<HTMLDivElement>(null);

// Step 1: Get payment credentials and show the container
const handlePayment = async () => {
  const paymentIntent = await createPaymentIntent();

  // Store credentials - don't call confirmPayment yet
  setPaymentCredentials({
    secret: paymentIntent.secret,
    publishable_key: paymentIntent.publishable_key,
    amount: paymentIntent.amount_to_pay,
    // ...
  });

  // Show the container
  setShowPaymentForm(true);
};

// Step 2: Initialize payment AFTER container is rendered
useEffect(() => {
  if (!showPaymentForm || !paymentCredentials) return;
  if (!paymentContainerRef.current) return;  // Container must exist

  const initPayment = async () => {
    await Khaime.confirmPayment({
      container: paymentContainerRef.current,  // Pass element directly
      secret: paymentCredentials.secret,
      // ...
    });
  };

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

// Step 3: Render container with ref
return (
  <>
    {showPaymentForm && (
      <div ref={paymentContainerRef} id="payment-container" />
    )}
  </>
);
```

<Tip>
  Pass the DOM element directly (`paymentContainerRef.current`) instead of an ID string to avoid race conditions.
</Tip>

***

## Order Summary: Transparency Best Practices

Always show a clear breakdown of costs:

```typescript theme={null}
// Order Summary Component
<div className="order-summary">
  {/* Line items */}
  {items.map(item => (
    <div key={item.id}>
      <span>{item.name} x {item.quantity}</span>
      <span>{formatCurrency(item.price * item.quantity)}</span>
    </div>
  ))}

  {/* Cost breakdown */}
  <div className="subtotal">
    <span>Subtotal</span>
    <span>{formatCurrency(subtotal)}</span>
  </div>

  <div className="shipping">
    <span>Shipping</span>
    <span>
      {shippingFee > 0
        ? formatCurrency(shippingFee)
        : orderTotal !== null
          ? 'Free'
          : 'Enter address'}
    </span>
  </div>

  <div className="total">
    <span>Total</span>
    <span>{formatCurrency(orderTotal ?? subtotal)}</span>
  </div>
</div>
```

### Format Currency Helper

```typescript theme={null}
function formatCurrency(amountInCents: number, currency = 'USD'): string {
  const amount = amountInCents / 100;  // Convert cents to dollars
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency,
    minimumFractionDigits: 2,
  }).format(amount);
}
```

***

## Complete Checkout Flow Example

```typescript theme={null}
// 1. User fills shipping form and clicks "Continue"
const handleShippingSubmit = async (e) => {
  e.preventDefault();
  setLoading(true);

  // Build cart items
  const cartItems = buildCartItems(items);

  // Build delivery details
  const deliveryDetails = buildDeliveryDetails(shippingInfo);

  // Validate cart to get shipping cost
  const cartResponse = await validateCart(cartItems, deliveryDetails);

  if (cartResponse.status) {
    // Update UI with shipping cost
    setShippingFee(cartResponse.data.shipping_fee);
    setOrderTotal(subtotal + cartResponse.data.shipping_fee);
    setCartUniqueId(cartResponse.data.cart_unique_id);

    // Move to payment step
    setStep('payment');
  }

  setLoading(false);
};

// 2. User clicks "Pay" button
const handlePayment = async () => {
  setLoading(true);

  // Create payment intent
  const paymentIntent = await createPaymentIntent({
    cart_unique_id: cartUniqueId,
    cart: cartItems,
    delivery_details: deliveryDetails,
    // ...
  });

  // Store credentials and show payment form
  setPaymentCredentials(paymentIntent.data);
  setShowPaymentForm(true);
  setLoading(false);
};

// 3. useEffect initializes payment when container is ready
useEffect(() => {
  if (!showPaymentForm || !paymentCredentials) return;
  if (!paymentContainerRef.current) return;

  Khaime.confirmPayment({
    container: paymentContainerRef.current,
    secret: paymentCredentials.secret,
    publishable_key: paymentCredentials.publishable_key,
    amount: paymentCredentials.amount_to_pay,
    currency: paymentCredentials.currency,
    display: 'inline',
    onSuccess: () => {
      clearCart();
      router.push('/checkout/success');
    },
    onError: (error) => setError(error.message),
  });
}, [showPaymentForm, paymentCredentials]);
```

***

## Troubleshooting

### "Container element not found"

**Cause**: `confirmPayment` called before React rendered the container.

**Fix**: Use `useEffect` to wait for the container to exist. See [React Integration](#react-integration-critical-timing-issue).

### Amount mismatch between frontend and payment

**Cause**: Backend adds shipping, fees, or taxes that frontend doesn't know about.

**Fix**:

1. Call `validateCart` with delivery details to get shipping
2. Display `amount_to_pay` from payment intent as the final total
3. Show shipping as a line item for transparency

### "main\_variant is not allowed to be empty"

**Cause**: Sending `main_variant: ''` for a product without variations.

**Fix**: Only include `main_variant` and `least_sub_variant_id` for products where `has_variation: true`.

### Unexpectedly high shipping

**Cause**: Shipping rules configured in dollars instead of cents, or per-item rates.

**Fix**: Check merchant shipping rules configuration. All amounts must be in cents.

***

***

## Multicurrency Support

Khaime supports 35+ currencies with automatic detection and real-time conversion.

### How It Works

1. **Auto-detect currency** from customer's IP (or let them manually select)
2. **Convert prices** using live exchange rates via `/pricing/calculate`
3. **Process payment** in customer's currency
4. **Merchant receives payout** in their baseline currency

<Error>
  **CRITICAL: Avoid Double-Conversion Bug**

  The backend API returns some values **already converted** to the target currency:

  * `shipping_fee` from validateCart and payment intent
  * `amount_to_pay` (orderTotal) from payment intent

  **DO NOT** apply frontend exchange rate conversion to these values. This causes prices to be multiplied twice.

  Example of the bug:

  * USD shipping: \$450
  * Exchange rate: 18x (MXN)
  * Backend returns: MX\$8,100 (correct)
  * Frontend converts again: MX$8,100 × 18 = MX$145,800 (WRONG!)
</Error>

### What Gets Converted Where

| Value                         | Converted By | Frontend Action                                  |
| ----------------------------- | ------------ | ------------------------------------------------ |
| Product prices                | Frontend     | Apply `exchangeRate`                             |
| Cart subtotal                 | Frontend     | Apply `exchangeRate`                             |
| Shipping fee                  | Backend API  | **Skip conversion** (already in target currency) |
| Order total (`amount_to_pay`) | Backend API  | **Skip conversion** (already in target currency) |

### The `skipConversion` Pattern

Implement a `formatPrice` function that can skip conversion:

```typescript theme={null}
const formatPrice = (amountInCents: number, skipConversion = false): string => {
  // Skip conversion if value is already in target currency
  const convertedAmount = (currency === baseCurrency || skipConversion)
    ? amountInCents
    : Math.round(amountInCents * exchangeRate);

  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: currency,
    minimumFractionDigits: 2,
  }).format(convertedAmount / 100);
};
```

### Displaying Prices Correctly

```typescript theme={null}
// Products and subtotal - apply conversion
{formatPrice(product.price)}           // Convert
{formatPrice(subtotal)}                // Convert

// Shipping - already converted by API
{formatPrice(shippingFee, true)}       // Skip conversion

// Total - depends on source
{orderTotal !== null
  ? formatPrice(orderTotal, true)      // API total - skip conversion
  : formatPrice(subtotal)              // No total yet - convert subtotal
}
```

### Calculating Total When API Doesn't Provide It

When the API doesn't return a total but you have shipping, you must manually combine:

```typescript theme={null}
// WRONG - mixing currencies!
const total = subtotal + shippingFee;  // subtotal is USD, shipping is MXN

// CORRECT - convert subtotal first, then add shipping
const convertedSubtotal = Math.round(subtotal * exchangeRate);
const total = convertedSubtotal + shippingFee;  // Both in MXN

// Display with skipConversion since we already converted
{formatPrice(total, true)}
```

### Detect Customer Currency

```typescript theme={null}
// Auto-detect from IP (omit target_currency)
const response = await fetch(
  `${API_BASE}/pricing/calculate?amount=10000`,
  { headers: { 'X-API-Key': API_KEY } }
);

const data = await response.json();
// data.customer.currency.code = "NGN" (detected)
// data.customer.location.country = "Nigeria"
// data.pricing.local = { amount: 16675000, currency: "NGN", formatted: "₦166,750.00" }
```

### Convert Price to Customer's Currency

```typescript theme={null}
const response = await fetch(
  `${API_BASE}/pricing/calculate?amount=${priceInCents}&target_currency=${customerCurrency}`,
  { headers: { 'X-API-Key': API_KEY } }
);

const data = await response.json();
const localPrice = data.pricing.local;
// localPrice.amount = converted amount in cents
// localPrice.formatted = "₦166,750.00" (ready to display)
```

### Currency Selector Component Pattern

```typescript theme={null}
// Store selected currency in context/state
const [currency, setCurrency] = useState('USD');
const [exchangeRate, setExchangeRate] = useState(1);

// On currency change, fetch new rate
useEffect(() => {
  const fetchRate = async () => {
    const response = await api.calculatePricing({
      amount: 10000,  // Sample amount
      target_currency: currency,
    });
    setExchangeRate(response.data.pricing.conversion.rate);
  };
  fetchRate();
}, [currency]);

// Display converted prices
const displayPrice = (amountInCents) => {
  const converted = Math.round(amountInCents * exchangeRate);
  return formatCurrency(converted, currency);
};
```

### Supported Currencies

| Region       | Currencies                                       |
| ------------ | ------------------------------------------------ |
| Americas     | USD, CAD, BRL, MXN, ARS, CLP                     |
| Europe       | EUR, GBP, CHF, SEK, NOK, DKK, PLN, CZK, HUF, RON |
| Africa       | NGN, GHS, KES, ZAR, XOF, XAF, TZS, UGX, RWF, ZMW |
| Middle East  | AED, SAR                                         |
| Asia-Pacific | AUD, NZD, JPY, CNY, INR, KRW, PHP                |

<Tip>
  For unambiguous currencies (NGN, GHS, KES), the currency code alone identifies the country. For ambiguous currencies (USD, EUR), use `customer_country` parameter if you need country-specific pricing rules.
</Tip>

***

## Merchant Configuration Checklist

Before going live, verify:

* [ ] Shipping rates are in **cents** (450 = $4.50, not $450)
* [ ] Shipping zones are configured correctly
* [ ] Tax settings are correct for your regions
* [ ] Platform fees are understood and accounted for
* [ ] Test with various cart sizes and addresses
* [ ] Multicurrency: Test with different currencies
* [ ] Currency selector is visible and functional
* [ ] Prices display correctly after currency conversion
