Khaime SDK Quick Reference
Critical Rules
Backend calculates, frontend displays
NEVER calculate prices on frontend. Display only what the API returns for subtotal, shipping, and total.
All amounts in CENTS
$4.50 = 450 cents
$45.00 = 4500 cents
$450.00 = 45000 centsShipping is server-side
Call
validateCart with delivery address to get shipping cost. Cannot calculate client-side.Wait for DOM
In React, use
useEffect to initialize payment AFTER container renders.SDK Methods
Load SDK
<script src="https://js.khaime.com/v1/"></script>
Confirm Payment
await Khaime.confirmPayment({
// Required
secret: 'pi_xxx_secret_yyy',
publishable_key: 'pk_live_xxx',
// Optional but recommended
stripe_account_id: 'acct_xxx', // For Connect
amount: 4500, // Display amount in CENTS
currency: 'USD',
// Display mode
display: 'inline', // 'modal' | 'inline'
container: element, // Required for inline mode
// Callbacks
onReady: () => {},
onSuccess: (result) => {},
onError: (error) => {},
});
Cart Item Schema
// Products WITHOUT variations
{
product_id: 123, // number, not string
quantity: 2,
price: 18500, // CENTS
has_variation: false,
product_image: 'https://...',
product_variant_data: 'none', // Must be 'none'
// DO NOT include: main_variant, least_sub_variant_id
}
// Products WITH variations
{
product_id: 123,
quantity: 1,
price: 2500,
has_variation: true,
product_image: 'https://...',
product_variant_data: 'Large / Blue',
main_variant: 'Large / Blue', // Include for variants
least_sub_variant_id: 'var_xxx', // Include for variants
}
Checkout Flow
1. SHIPPING STEP
└─> User enters address
└─> Click "Continue"
└─> Call validateCart(cart, deliveryDetails)
└─> Response: { shipping_fee: 450, cart_unique_id: 'xxx' }
└─> Update UI: setShippingFee(450)
2. PAYMENT STEP
└─> User clicks "Pay"
└─> Call createPaymentIntent(cart_unique_id, ...)
└─> Response: { secret, publishable_key, amount_to_pay }
└─> setCredentials(response) + setShowForm(true)
3. PAYMENT FORM (useEffect)
└─> Wait for container to exist
└─> Call Khaime.confirmPayment({ container, ... })
└─> onSuccess: clearCart + redirect
React Pattern
const [credentials, setCredentials] = useState(null);
const [showForm, setShowForm] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// Get credentials, then show form
const handlePay = async () => {
const intent = await createPaymentIntent(...);
setCredentials(intent.data);
setShowForm(true); // Container renders
};
// Initialize AFTER container exists
useEffect(() => {
if (!showForm || !credentials || !containerRef.current) return;
Khaime.confirmPayment({
container: containerRef.current, // Pass element, not ID
secret: credentials.secret,
publishable_key: credentials.publishable_key,
amount: credentials.amount_to_pay,
display: 'inline',
onSuccess: () => router.push('/success'),
});
}, [showForm, credentials]);
return showForm && <div ref={containerRef} />;
Validation Errors
| Error | Cause | Fix |
|---|---|---|
main_variant not allowed to be empty | Sent main_variant for non-variant product | Only include for has_variation: true |
product_variant_data is required | Missing field | Use 'none' for non-variant products |
least_sub_variant_id not allowed | Sent variant ID for non-variant product | Only include for has_variation: true |
Container element not found | DOM not ready | Use useEffect, pass element not ID |
Multicurrency (Critical)
Double-Conversion Bug: Backend returns
shipping_fee and amount_to_pay ALREADY CONVERTED. Do NOT apply exchange rate again!What to convert vs skip
// Frontend converts these (in base currency)
formatPrice(product.price) // Convert
formatPrice(subtotal) // Convert
// Backend already converted these (in target currency)
formatPrice(shippingFee, true) // SKIP conversion
formatPrice(orderTotal, true) // SKIP conversion
formatPrice with skipConversion
const formatPrice = (cents: number, skipConversion = false): string => {
const amount = (currency === baseCurrency || skipConversion)
? cents
: Math.round(cents * exchangeRate);
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency,
}).format(amount / 100);
};
Combining different currencies
// When API doesn't provide total, calculate manually:
const convertedSubtotal = Math.round(subtotal * exchangeRate); // USD → target
const total = convertedSubtotal + shippingFee; // Both in target now
formatPrice(total, true); // Skip - already converted
Display Currency (Single Currency)
function formatCurrency(cents: number, currency = 'USD'): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency,
}).format(cents / 100);
}
// Usage
formatCurrency(4500); // "$45.00"
formatCurrency(45000); // "$450.00"
Shipping Rules
4 destination types (priority order):| Type | Priority | Meaning |
|---|---|---|
specific_locations | 1 (highest) | Specific cities/areas within states |
specific_states | 2 | One or more configured states |
nationwide | 3 | Any domestic destination |
international | 4 (fallback) | Outside merchant’s country |
// Location in pricing_summary.shipping_details
const { matched_rules, available_destinations } = response.data.pricing_summary.shipping_details;
// Build state/location selectors from available_destinations
const locationsForState = available_destinations
.filter(opt => opt.destination_type === 'specific_locations' && opt.destination_states.includes(selectedState))
.flatMap(opt => opt.destination_locations);
API Response Fields
validateCart Response
{
status: true,
data: {
cart_unique_id: 'cart_xxx', // Use in payment intent
shipping_fee: 450, // CENTS (from matched_rules)
shipping_details: {
matched_rules: [{ rule_id: 58, rule_name: 'Nationwide', destination_type: 'nationwide' }],
available_destinations: [
{ rule_id: 56, rule_name: 'Dallas Rate', destination_type: 'specific_locations', shipping_fee: 350 },
{ rule_id: 58, rule_name: 'Nationwide', destination_type: 'nationwide', shipping_fee: 450 }
]
}
}
}
createPaymentIntent Response
{
status: true,
data: {
secret: 'pi_xxx_secret_yyy',
publishable_key: 'pk_xxx',
amount_to_pay: 5000, // Final amount in CENTS
currency: 'USD',
data: {
stripe_account_id: 'acct_xxx',
}
}
}
