# Khaime Commerce API - Complete Implementation Guide for AI Agents This document provides everything needed to build a complete e-commerce frontend with the Khaime API. It includes copy-paste ready code, framework configurations, and solutions to every common problem. ================================================================================ TABLE OF CONTENTS ================================================================================ 1. CRITICAL CONFIGURATION (Read First!) 2. COMPLETE PROJECT SETUP (Next.js) 3. API CLIENT IMPLEMENTATION 4. PRODUCT DISPLAY COMPONENTS 5. PRODUCT VARIATIONS (Color/Size Selectors) 6. SHOPPING CART IMPLEMENTATION 7. CHECKOUT FLOW 8. CUSTOMER AUTHENTICATION 9. ORDER HISTORY 10. WEBHOOKS 11. TROUBLESHOOTING GUIDE 12. COMPLETE TYPE DEFINITIONS ================================================================================ SECTION 1: CRITICAL CONFIGURATION (READ THIS FIRST!) ================================================================================ Before writing ANY code, understand these 5 critical rules: ┌─────────────────────────────────────────────────────────────────────────────┐ │ RULE 1: BASE URL MUST INCLUDE /api/v1 │ │ │ │ CORRECT: https://api.khaime.com/api/v1 │ │ WRONG: https://api.khaime.com │ │ │ │ Without /api/v1, all requests will return 404. │ └─────────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────────┐ │ RULE 2: ALL PRICES ARE IN CENTS/KOBO/PENCE │ │ │ │ API returns: { price: 6000, currency: "USD" } │ │ Display as: $60.00 (divide by 100!) │ │ │ │ NEVER display the raw number - it will be 100x too high! │ └─────────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────────┐ │ RULE 3: PRODUCTS ARE NESTED IN data.products │ │ │ │ CORRECT: const products = response.data.products; │ │ WRONG: const products = response.data; │ │ │ │ The data field contains { products: [], pagination: {} } │ └─────────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────────┐ │ RULE 4: USE THE CORRECT FIELD NAMES │ │ │ │ WRONG CORRECT │ │ product.name product.title (it's "title", not "name") │ │ product.stock product.total_quantity (it's "total_quantity") │ │ product.id (str) product.id (number) (ID is a NUMBER) │ │ product.variants product.product_variation.variations (nested!) │ └─────────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────────┐ │ RULE 5: CONFIGURE IMAGE DOMAINS (Next.js) │ │ │ │ Images are hosted on CloudFront. Add to next.config.js: │ │ │ │ images: { │ │ remotePatterns: [ │ │ { protocol: 'https', hostname: 'd2ipu1j7d156ey.cloudfront.net' } │ │ ] │ │ } │ └─────────────────────────────────────────────────────────────────────────────┘ ================================================================================ SECTION 2: COMPLETE PROJECT SETUP (NEXT.JS) ================================================================================ ## 2.1 Environment Variables Create `.env.local` in your project root: ```bash # Khaime API Configuration # IMPORTANT: URL must end with /api/v1 NEXT_PUBLIC_KHAIME_API_URL=https://api.khaime.com/api/v1 NEXT_PUBLIC_KHAIME_API_KEY=pk_sandbox_your_key_here # Payment SDK (optional, defaults shown) NEXT_PUBLIC_KHAIME_SDK_URL=https://js.khaime.com/v1/ ``` ## 2.2 Next.js Configuration Create or update `next.config.ts`: ```typescript import type { NextConfig } from 'next'; const nextConfig: NextConfig = { images: { remotePatterns: [ { protocol: 'https', hostname: 'd2ipu1j7d156ey.cloudfront.net', pathname: '/**', }, ], }, }; export default nextConfig; ``` ## 2.3 Project Structure (Recommended) ``` src/ ├── app/ │ ├── layout.tsx # Root layout with providers │ ├── page.tsx # Homepage with featured products │ ├── products/ │ │ ├── page.tsx # Product listing │ │ └── [id]/ │ │ └── page.tsx # Product detail with variations │ ├── cart/ │ │ └── page.tsx # Shopping cart │ ├── checkout/ │ │ ├── page.tsx # Checkout form │ │ └── success/ │ │ └── page.tsx # Order confirmation │ ├── auth/ │ │ └── page.tsx # Login/Register │ └── orders/ │ └── page.tsx # Order history ├── components/ │ ├── ProductCard.tsx # Product card for listings │ ├── ProductVariations.tsx # Color/size selector │ ├── Header.tsx # Navigation with cart count │ └── Footer.tsx ├── contexts/ │ ├── CartContext.tsx # Shopping cart state │ └── AuthContext.tsx # Customer authentication ├── lib/ │ ├── api.ts # Khaime API client │ └── utils.ts # Price formatting, etc. └── types/ └── index.ts # TypeScript interfaces ``` ================================================================================ SECTION 3: API CLIENT IMPLEMENTATION ================================================================================ ## 3.1 Complete API Client (src/lib/api.ts) ```typescript import type { Product, CartValidationResponse, PaymentIntent, Order, AuthResponse, ApiResponse, } from '@/types'; // CRITICAL: URL must include /api/v1 const API_URL = process.env.NEXT_PUBLIC_KHAIME_API_URL || 'https://api.khaime.com/api/v1'; const API_KEY = process.env.NEXT_PUBLIC_KHAIME_API_KEY || ''; class KhaimeApi { private baseUrl: string; private apiKey: string; private authToken: string | null = null; constructor(baseUrl: string, apiKey: string) { this.baseUrl = baseUrl; this.apiKey = apiKey; } setAuthToken(token: string | null) { this.authToken = token; } private async request( endpoint: string, options: RequestInit = {} ): Promise> { const headers: HeadersInit = { 'Content-Type': 'application/json', 'X-API-Key': this.apiKey, ...options.headers, }; if (this.authToken) { (headers as Record)['Authorization'] = `Bearer ${this.authToken}`; } try { const response = await fetch(`${this.baseUrl}${endpoint}`, { ...options, headers, }); const data = await response.json(); if (!response.ok) { return { status: false, message: data.message || 'An error occurred', error_code: data.error_code || 'UNKNOWN_ERROR', }; } return { status: true, data: data.data || data, }; } catch (error) { return { status: false, message: error instanceof Error ? error.message : 'Network error', error_code: 'NETWORK_ERROR', }; } } // ========================================================================= // PRODUCTS // ========================================================================= async getProducts(params?: { currency?: string; country?: string; category?: string; page?: number; limit?: number; }): Promise> { const searchParams = new URLSearchParams(); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { searchParams.append(key, String(value)); } }); } const query = searchParams.toString(); return this.request(`/products${query ? `?${query}` : ''}`); } async getProduct(id: string | number): Promise> { return this.request(`/product/${id}`); } // ========================================================================= // CUSTOMER AUTH // ========================================================================= async register(data: { email: string; password: string; first_name?: string; last_name?: string; }): Promise> { return this.request('/register', { method: 'POST', body: JSON.stringify(data), }); } async login(data: { email: string; password: string; }): Promise> { return this.request('/login', { method: 'POST', body: JSON.stringify(data), }); } // ========================================================================= // CART & CHECKOUT // ========================================================================= async validateCart( items: { product_id: number; // NUMBER, not string! variant_id?: string; least_sub_variant_id?: string; quantity: number; price: number; }[], currency: string = 'USD', discount_code?: string ): Promise> { return this.request('/cart/validate', { method: 'POST', body: JSON.stringify({ cart: items, currency, coupons: discount_code ? [discount_code] : undefined, }), }); } // CRITICAL: All these fields are REQUIRED for payment intent! async createPaymentIntent(data: { // Required user info first_name: string; // REQUIRED last_name: string; // REQUIRED email: string; // REQUIRED - lowercase! // Required payment info payment_type: 'one' | 'multiple_time' | 'subscription' | 'renew_access'; // REQUIRED payment_by?: 'customer' | 'merchant'; // Defaults to 'customer' is_coupon_used: boolean; // REQUIRED is_second_time_payment?: boolean; // For returning customers // Required product info product_title: string; // REQUIRED - displayed on payment page product_type: string; // REQUIRED - e.g., "physical_product", "digital" // Cart info cart_unique_id?: string; // From cart validation response currency: string; // REQUIRED // Optional but recommended order_total_charge?: number; // Total in cents // Addresses (for physical products) shipping_address?: { line1: string; line2?: string; city: string; state: string; postal_code: string; country: string; }; billing_address?: { line1: string; line2?: string; city: string; state: string; postal_code: string; country: string; }; }): Promise> { return this.request('/payment/intent', { method: 'POST', body: JSON.stringify(data), }); } // ========================================================================= // ORDERS (requires auth token) // ========================================================================= async getOrders(): Promise> { return this.request('/orders'); } async getOrder(id: string): Promise> { return this.request(`/order/${id}`); } } export const khaimeApi = new KhaimeApi(API_URL, API_KEY); export default khaimeApi; ``` ## 3.2 Utility Functions (src/lib/utils.ts) ```typescript /** * Format price from cents to display currency * CRITICAL: API returns prices in cents - MUST divide by 100! */ export function formatCurrency(amountInCents: number, currency: string = 'USD'): string { const amount = amountInCents / 100; // <-- CRITICAL: Divide by 100! return new Intl.NumberFormat('en-US', { style: 'currency', currency, minimumFractionDigits: 2, }).format(amount); } /** * Calculate variant price * Final price = base price + variation_add_price + subvariant_add_price * ALL VALUES IN CENTS */ export function calculateVariantPrice( basePrice: number, variationAddPrice: number = 0, subvariantAddPrice: number = 0 ): number { return basePrice + variationAddPrice + subvariantAddPrice; } /** * Get display type for variant */ export function getVariantDisplayType(variantObject: string): 'color' | 'image' | 'text' { if (variantObject?.match(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/)) { return 'color'; } if (variantObject?.startsWith('http')) { return 'image'; } return 'text'; } ``` ================================================================================ SECTION 4: PRODUCT DISPLAY COMPONENTS ================================================================================ ## 4.1 Product Card Component (src/components/ProductCard.tsx) ```tsx 'use client'; import Image from 'next/image'; import Link from 'next/link'; import type { Product } from '@/types'; import { formatCurrency } from '@/lib/utils'; import { useCart } from '@/contexts/CartContext'; interface ProductCardProps { product: Product; } export default function ProductCard({ product }: ProductCardProps) { const { addItem } = useCart(); const handleAddToCart = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); addItem(product, undefined, 1); }; return (
{/* Image - Use product.title for alt, NOT product.name! */}
{product.images && product.images.length > 0 ? ( {product.title} ) : (
No Image
)}
{/* Content */}

{product.title}

{product.category && (

{product.category}

)}
{/* Price - formatCurrency divides by 100 */} {formatCurrency(product.price, product.currency)}
); } ``` ## 4.2 Products Page (src/app/products/page.tsx) ```tsx import khaimeApi from '@/lib/api'; import ProductCard from '@/components/ProductCard'; import type { Product } from '@/types'; async function getProducts(): Promise { const response = await khaimeApi.getProducts({ limit: 20 }); if (response.status && response.data) { // CRITICAL: Products are nested in data.products! return response.data.products || []; } return []; } export default async function ProductsPage() { const products = await getProducts(); return (

All Products

{products.length === 0 ? (

No products found.

) : (
{products.map((product) => ( ))}
)}
); } ``` ================================================================================ SECTION 5: PRODUCT VARIATIONS (COLOR/SIZE SELECTORS) ================================================================================ This is one of the most complex parts. Products can have TWO levels of variations. ## 5.1 Understanding the Structure ``` Product ├── price: 2999 (base price in cents) ├── has_variation: true (CHECK THIS FIRST!) └── product_variation ├── variation_type: "color" | "size" | "custom" └── variations[] ├── id: "var_red" ├── variant_label: "Red" ├── variant_object: "#FF0000" (hex color OR image URL) ├── variation_add_price: 500 (cents added to base) ├── variation_quantity: 50 (stock) ├── has_sub_variants: true └── sub_variation ├── sub_variation_type: "size" └── sub_variants[] ├── id: "var_red_large" (USE THIS for cart!) ├── variant_label: "Large" ├── variant_quantity: 20 (stock) └── subvariant_add_price: 200 (cents added) ``` ## 5.2 Complete Variation Selector Component ```tsx 'use client'; import { useState, useEffect } from 'react'; import Image from 'next/image'; import type { Product, Variation, SubVariant } from '@/types'; import { formatCurrency, getVariantDisplayType } from '@/lib/utils'; interface VariationSelectorProps { product: Product; onSelectionChange: (selection: { variantId: string; variantLabel: string; totalPrice: number; stock: number; }) => void; } export default function VariationSelector({ product, onSelectionChange, }: VariationSelectorProps) { const [selectedVariationIndex, setSelectedVariationIndex] = useState(0); const [selectedSubVariantIndex, setSelectedSubVariantIndex] = useState(0); // Don't render if no variations if (!product.has_variation || !product.product_variation?.variations?.length) { return null; } const variations = product.product_variation.variations; const currentVariation = variations[selectedVariationIndex]; const subVariants = currentVariation?.sub_variation?.sub_variants || []; const currentSubVariant = subVariants[selectedSubVariantIndex]; // Calculate price and get correct ID useEffect(() => { let totalPrice = product.price; let variantId = currentVariation.id; let variantLabel = currentVariation.variant_label; let stock = currentVariation.variation_quantity; totalPrice += currentVariation.variation_add_price || 0; if (currentVariation.has_sub_variants && currentSubVariant) { totalPrice += currentSubVariant.subvariant_add_price || 0; variantId = currentSubVariant.id; // <-- Use sub-variant ID for cart! variantLabel = `${currentVariation.variant_label} / ${currentSubVariant.variant_label}`; stock = currentSubVariant.variant_quantity; } onSelectionChange({ variantId, variantLabel, totalPrice, stock }); }, [selectedVariationIndex, selectedSubVariantIndex, product, currentVariation, currentSubVariant, onSelectionChange]); // Handle variation selection const handleVariationSelect = (index: number) => { setSelectedVariationIndex(index); setSelectedSubVariantIndex(0); // Reset sub-variant when main changes! }; return (
{/* Primary Variations (e.g., Color) */}
{variations.map((variation, index) => { const isSelected = selectedVariationIndex === index; const displayType = getVariantDisplayType(variation.variant_object); const isOutOfStock = variation.variation_quantity === 0; // Color swatch if (displayType === 'color') { return ( ); } // Text button (size, custom) return ( ); })}
{/* Sub-Variations (e.g., Size within Color) */} {currentVariation.has_sub_variants && subVariants.length > 0 && (
{subVariants.map((subVariant, index) => { const isSelected = selectedSubVariantIndex === index; const isOutOfStock = subVariant.variant_quantity === 0; return ( ); })}
)}
); } ``` ## 5.3 Using Variations in Product Detail Page ```tsx 'use client'; import { useState, useCallback } from 'react'; import VariationSelector from '@/components/VariationSelector'; import { formatCurrency } from '@/lib/utils'; import { useCart } from '@/contexts/CartContext'; export default function ProductDetail({ product }) { const { addItem } = useCart(); const [selection, setSelection] = useState({ variantId: '', variantLabel: '', totalPrice: product.price, stock: product.total_quantity, }); const handleSelectionChange = useCallback((newSelection) => { setSelection(newSelection); }, []); const handleAddToCart = () => { addItem(product, { id: selection.variantId, name: selection.variantLabel, price: selection.totalPrice, stock: selection.stock, }, 1); }; return (

{product.title}

{formatCurrency(selection.totalPrice, product.currency)}

{selection.stock > 0 ? `${selection.stock} in stock` : 'Out of stock'}

); } ``` ================================================================================ SECTION 6: SHOPPING CART IMPLEMENTATION ================================================================================ ## 6.1 Cart Context (src/contexts/CartContext.tsx) ```tsx 'use client'; import React, { createContext, useContext, useReducer, useEffect, ReactNode } from 'react'; import type { Product } from '@/types'; interface CartVariant { id: string; name: string; price: number; // Total price in cents (base + add prices) stock?: number; } interface CartItem { product: Product; variant?: CartVariant; quantity: number; } interface CartState { items: CartItem[]; itemCount: number; subtotal: number; // In cents currency: string; } type CartAction = | { type: 'ADD_ITEM'; product: Product; variant?: CartVariant; quantity: number } | { type: 'REMOVE_ITEM'; productId: number; variantId?: string } // productId is NUMBER | { type: 'UPDATE_QUANTITY'; productId: number; variantId?: string; quantity: number } | { type: 'CLEAR_CART' } | { type: 'LOAD_CART'; state: CartState }; interface CartContextType extends CartState { addItem: (product: Product, variant?: CartVariant, quantity?: number) => void; removeItem: (productId: number, variantId?: string) => void; updateQuantity: (productId: number, quantity: number, variantId?: string) => void; clearCart: () => void; } const CartContext = createContext(undefined); function calculateSubtotal(items: CartItem[]): number { return items.reduce((sum, item) => { // Use variant price if available, otherwise base product price const price = item.variant?.price ?? item.product.price; return sum + price * item.quantity; }, 0); } function cartReducer(state: CartState, action: CartAction): CartState { switch (action.type) { case 'ADD_ITEM': { const existingIndex = state.items.findIndex( (item) => item.product.id === action.product.id && item.variant?.id === action.variant?.id ); let newItems: CartItem[]; if (existingIndex >= 0) { newItems = state.items.map((item, index) => index === existingIndex ? { ...item, quantity: item.quantity + action.quantity } : item ); } else { newItems = [ ...state.items, { product: action.product, variant: action.variant, quantity: action.quantity, }, ]; } return { ...state, items: newItems, itemCount: newItems.reduce((sum, item) => sum + item.quantity, 0), subtotal: calculateSubtotal(newItems), currency: action.product.currency || state.currency, }; } case 'REMOVE_ITEM': { const newItems = state.items.filter( (item) => !(item.product.id === action.productId && item.variant?.id === action.variantId) ); return { ...state, items: newItems, itemCount: newItems.reduce((sum, item) => sum + item.quantity, 0), subtotal: calculateSubtotal(newItems), }; } case 'UPDATE_QUANTITY': { if (action.quantity <= 0) { return cartReducer(state, { type: 'REMOVE_ITEM', productId: action.productId, variantId: action.variantId, }); } const newItems = state.items.map((item) => item.product.id === action.productId && item.variant?.id === action.variantId ? { ...item, quantity: action.quantity } : item ); return { ...state, items: newItems, itemCount: newItems.reduce((sum, item) => sum + item.quantity, 0), subtotal: calculateSubtotal(newItems), }; } case 'CLEAR_CART': return { items: [], itemCount: 0, subtotal: 0, currency: state.currency }; case 'LOAD_CART': return action.state; default: return state; } } const CART_STORAGE_KEY = 'khaime_cart'; export function CartProvider({ children }: { children: ReactNode }) { const [state, dispatch] = useReducer(cartReducer, { items: [], itemCount: 0, subtotal: 0, currency: 'USD', }); // Load from localStorage useEffect(() => { const saved = localStorage.getItem(CART_STORAGE_KEY); if (saved) { try { dispatch({ type: 'LOAD_CART', state: JSON.parse(saved) }); } catch { localStorage.removeItem(CART_STORAGE_KEY); } } }, []); // Save to localStorage useEffect(() => { localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(state)); }, [state]); const addItem = (product: Product, variant?: CartVariant, quantity = 1) => { dispatch({ type: 'ADD_ITEM', product, variant, quantity }); }; const removeItem = (productId: number, variantId?: string) => { dispatch({ type: 'REMOVE_ITEM', productId, variantId }); }; const updateQuantity = (productId: number, quantity: number, variantId?: string) => { dispatch({ type: 'UPDATE_QUANTITY', productId, variantId, quantity }); }; const clearCart = () => { dispatch({ type: 'CLEAR_CART' }); }; return ( {children} ); } export function useCart() { const context = useContext(CartContext); if (!context) { throw new Error('useCart must be used within CartProvider'); } return context; } ``` ================================================================================ SECTION 7: CHECKOUT FLOW ================================================================================ ## 7.1 Checkout Page CRITICAL: The payment intent endpoint requires many fields. Missing ANY of these will cause validation errors. This is the complete working implementation: ```tsx 'use client'; import { useState } from 'react'; import { useRouter } from 'next/navigation'; import Script from 'next/script'; import { useCart } from '@/contexts/CartContext'; import khaimeApi from '@/lib/api'; import { formatCurrency } from '@/lib/utils'; declare global { interface Window { Khaime?: { confirmPayment: (clientSecret: string, options?: Record) => Promise<{ error?: { message: string } }>; }; } } export default function CheckoutPage() { const router = useRouter(); const { items, subtotal, currency, clearCart } = useCart(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [form, setForm] = useState({ email: '', firstName: '', lastName: '', address: '', city: '', state: '', postalCode: '', country: 'US', }); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); setError(null); try { // ===================================================================== // STEP 1: VALIDATE CART // ALL these fields are REQUIRED by the API // ===================================================================== const cartItems = items.map((item) => ({ product_id: item.product.id, // NUMBER, not string! quantity: item.quantity, price: item.variant?.price ?? item.product.price, has_variation: item.product.has_variation, // REQUIRED! least_sub_variant_id: item.variant?.id, // Required when has_variation is true product_image: item.product.images?.[0] || '', // REQUIRED! product_variant_data: item.variant?.name || '', // REQUIRED! Variant label })); const cartResponse = await khaimeApi.validateCart(cartItems, currency); if (!cartResponse.status) { setError(cartResponse.message); setLoading(false); return; } // ===================================================================== // STEP 2: CREATE PAYMENT INTENT // ALL THESE FIELDS ARE REQUIRED! Missing any will cause validation error // ===================================================================== const firstProduct = items[0]?.product; const productTitle = items.length === 1 ? firstProduct?.title : `${firstProduct?.title} + ${items.length - 1} more`; const paymentResponse = await khaimeApi.createPaymentIntent({ // REQUIRED: User info first_name: form.firstName, last_name: form.lastName, email: form.email.toLowerCase(), // REQUIRED: Payment info payment_type: 'one', // "one" | "multiple_time" | "subscription" payment_by: 'customer', is_coupon_used: false, is_second_time_payment: false, // REQUIRED: Product info product_title: productTitle, product_type: firstProduct?.product_type || 'physical_product', // REQUIRED: Cart info cart_unique_id: cartResponse.data.cart_unique_id, currency: currency, // Optional but recommended order_total_charge: subtotal, // Addresses (for physical products) shipping_address: { line1: form.address, city: form.city, state: form.state, postal_code: form.postalCode, country: form.country, }, billing_address: { line1: form.address, city: form.city, state: form.state, postal_code: form.postalCode, country: form.country, }, }); if (!paymentResponse.status) { setError(paymentResponse.message); setLoading(false); return; } // ===================================================================== // STEP 3: CONFIRM PAYMENT // ===================================================================== const { client_secret, payment_url } = paymentResponse.data; // Try embedded flow first (USD, EUR, etc.) if (window.Khaime && client_secret) { const result = await window.Khaime.confirmPayment(client_secret); if (result.error) { setError(result.error.message); setLoading(false); return; } clearCart(); router.push('/checkout/success'); } // Fallback to redirect flow (NGN, etc.) else if (payment_url) { clearCart(); window.location.href = payment_url; } else { setError('Unable to process payment.'); setLoading(false); } } catch (err) { setError(err instanceof Error ? err.message : 'Payment failed'); setLoading(false); } }; return ( <>