Official SDK for building e-commerce storefronts with OmniSync Platform. Perfect for vibe-coded sites, AI-built stores (Cursor, Lovable, v0), and custom storefronts.
npm install omni-sync-sdkOfficial SDK for building e-commerce storefronts with OmniSync Platform.
This SDK provides a complete solution for vibe-coded sites, AI-built stores (Cursor, Lovable, v0), and custom storefronts to connect to OmniSync's unified commerce API.
> 🤖 AI Agents / Vibe Coders: See AI_BUILDER_PROMPT.md for a concise, copy-paste-ready prompt optimized for AI code generation. It contains the essential rules and complete code examples to build a working store.
``bash`
npm install omni-sync-sdkor
pnpm add omni-sync-sdkor
yarn add omni-sync-sdk
---
The SDK exports these utility functions for common UI tasks:
| Function | Purpose | Example |
| ---------------------------------------------- | -------------------------------------- | ------------------------------------------------------ |
| formatPrice(amount, { currency?, locale? }) | Format prices for display | formatPrice("99.99", { currency: 'USD' }) → $99.99 |getPriceDisplay(amount, currency?, locale?)
| | Alias for formatPrice | Same as above |getDescriptionContent(product)
| | Get product description (HTML or text) | getDescriptionContent(product) |isHtmlDescription(product)
| | Check if description is HTML | isHtmlDescription(product) → true/false |getStockStatus(inventory)
| | Get human-readable stock status | getStockStatus(inventory) → "In Stock" |getProductPrice(product)
| | Get effective price (handles sales) | getProductPrice(product) → 29.99 |getProductPriceInfo(product)
| | Get price + sale info + discount % | { price, isOnSale, discountPercent } |getVariantPrice(variant, basePrice)
| | Get variant price with fallback | getVariantPrice(variant, '29.99') → 34.99 |getCartTotals(cart, shippingPrice?)
| | Calculate cart subtotal/discount/total | { subtotal, discount, shipping, total } |getCartItemName(item)
| | Get name from nested cart item | getCartItemName(item) → "Blue T-Shirt" |getCartItemImage(item)
| | Get image URL from cart item | getCartItemImage(item) → "https://..." |getVariantOptions(variant)
| | Get variant attributes as array | [{ name: "Color", value: "Red" }] |isCouponApplicableToProduct(coupon, product)
| | Check if coupon applies | isCouponApplicableToProduct(coupon, product) |
`typescript
import {
formatPrice,
getDescriptionContent,
getStockStatus,
getProductPrice,
getProductPriceInfo,
getCartTotals,
getCartItemName,
getCartItemImage,
} from 'omni-sync-sdk';
// Format price for display
const priceText = formatPrice(product.basePrice, { currency: 'USD' }); // "$99.99"
// Get product description (handles HTML vs plain text)
const description = getDescriptionContent(product);
// Get stock status text
const stockText = getStockStatus(product.inventory); // "In Stock", "Low Stock", "Out of Stock"
// Get effective price (handles sale prices automatically)
const price = getProductPrice(product); // Returns number: 29.99
// Get full price info including sale status
const priceInfo = getProductPriceInfo(product);
// { price: 19.99, originalPrice: 29.99, isOnSale: true, discountPercent: 33 }
// Calculate cart totals
const totals = getCartTotals(cart, shippingRate?.price);
// { subtotal: 59.98, discount: 10, shipping: 5.99, total: 55.97 }
// Access cart item details (handles nested structure)
const itemName = getCartItemName(cartItem); // "Blue T-Shirt - Large"
const itemImage = getCartItemImage(cartItem); // "https://..."
`
> ⚠️ DO NOT CREATE YOUR OWN UTILITY FILES! All helper functions above are exported from omni-sync-sdk. Never create utils/format.ts, lib/helpers.ts, or similar files - use the SDK exports directly.
---
Your store will NOT work without payment integration. The store owner has already configured payment providers (Stripe/PayPal) - you just need to implement the payment page.
`typescript
// On your checkout/payment page, ALWAYS call this first:
const { hasPayments, providers } = await omni.getPaymentProviders();
if (!hasPayments) {
// Show error - payment is not configured
return
// Show payment forms for available providers
const stripeProvider = providers.find(p => p.provider === 'stripe');
const paypalProvider = providers.find(p => p.provider === 'paypal');
`
See the Payment Integration section for complete implementation examples.
---
`typescript
import { OmniSyncClient } from 'omni-sync-sdk';
// Initialize with your Connection ID
const omni = new OmniSyncClient({
connectionId: 'vc_YOUR_CONNECTION_ID',
});
// Fetch products
const { data: products } = await omni.getProducts();
`
---
> AI Agents / Vibe-Coders: Read this section carefully! These are common misunderstandings.
This is the #1 cause of "Cart not found" errors!
`typescript
// ❌ WRONG - Local cart ID "__local__" doesn't exist on server!
const cart = await omni.smartGetCart(); // Returns { id: "__local__", ... }
const checkout = await omni.createCheckout({ cartId: cart.id }); // 💥 ERROR: Cart not found
// ✅ CORRECT - Use startGuestCheckout() for guest users
const result = await omni.startGuestCheckout();
if (result.tracked) {
const checkout = await omni.getCheckout(result.checkoutId);
// Continue with payment flow...
}
// ✅ ALTERNATIVE - Use submitGuestOrder() for simple checkout without payment UI
const order = await omni.submitGuestOrder();
`
Rule of thumb:
- Guest user + Local cart → startGuestCheckout() or submitGuestOrder()createCheckout({ cartId })
- Logged-in user + Server cart →
This causes type errors and runtime bugs!
`typescript
// ❌ WRONG - Don't create your own interfaces!
interface CartItem {
id: string;
name: string; // WRONG - it's item.product.name!
price: number; // WRONG - prices are strings!
}
// ❌ WRONG - Don't use 'as unknown as' casting!
const item = result as unknown as MyLocalType;
// ✅ CORRECT - Import ALL types from SDK
import type {
Product,
ProductVariant,
Cart,
CartItem,
Checkout,
CheckoutLineItem,
Order,
OrderItem,
CustomerProfile,
CustomerAddress,
ShippingRate,
PaymentProvider,
PaymentIntent,
PaymentStatus,
SearchSuggestions,
ProductSuggestion,
CategorySuggestion,
OAuthAuthorizeResponse,
CustomerOAuthProvider,
} from 'omni-sync-sdk';
`
⚠️ SDK Type Facts - Trust These!
| What | Correct | Wrong |
| ------------------------ | ----------------------------- | --------------------- |
| Prices | string (use parseFloat()) | number |item.product.name
| Cart item name | | item.name |item.name
| Order item name | | item.product.name |item.product.images[0]
| Cart item image | | item.image |item.image
| Order item image | | item.product.images |region
| Address state/province | | state or province |authorizationUrl
| OAuth redirect URL | | url |{ providers: [...] }
| OAuth providers response | | [...] directly |
If you think a type is "wrong", YOU are wrong. Read the SDK types!
`typescript
// ❌ WRONG
formatPrice(amount, 'USD');
// ✅ CORRECT
formatPrice(amount, { currency: 'USD' });
`
IMPORTANT: Cart and Checkout items have NESTED product data. Order items are FLAT.
`typescript
// CartItem and CheckoutLineItem - NESTED product
cart.items.forEach((item) => {
console.log(item.product.name); // ✅ Correct for Cart/Checkout
console.log(item.product.sku);
console.log(item.product.images);
});
// OrderItem - FLAT structure
order.items.forEach((item) => {
console.log(item.name); // ✅ Correct for Orders
console.log(item.sku);
console.log(item.image); // singular, not images
});
`
| Type | Access Name | Access Image |
| ------------------ | ------------------- | --------------------- |
| CartItem | item.product.name | item.product.images |CheckoutLineItem
| | item.product.name | item.product.images |OrderItem
| | item.name | item.image |
`typescript
// ❌ WRONG
if (status.status === 'completed')
// ✅ CORRECT
if (status.status === 'succeeded')
`
getSearchSuggestions() returns ProductSuggestion[], NOT Product[].
This is intentional - suggestions are lightweight for autocomplete.
`typescript
// ProductSuggestion has:
{
(id, name, slug, image, basePrice, salePrice, type);
}
// Product has many more fields
`
`typescript
// ❌ WRONG - assuming number
const total = item.price * quantity;
// ✅ CORRECT - parse first
const total = parseFloat(item.price) * quantity;
// Or use SDK helper
import { formatPrice } from 'omni-sync-sdk';
const display = formatPrice(item.price, { currency: 'USD' });
`
`typescript`
// Accessing variant attributes:
const color = variant.attributes?.['Color']; // string
const size = variant.attributes?.['Size']; // string
`typescript
// ❌ WRONG
const address = {
state: 'NY', // This field doesn't exist!
};
// ✅ CORRECT
const address: SetShippingAddressDto = {
firstName: 'John',
lastName: 'Doe',
line1: '123 Main St',
city: 'New York',
region: 'NY', // Use 'region' for state/province
postalCode: '10001',
country: 'US',
};
`
`typescript
// ❌ WRONG
const response = await omni.getOAuthAuthorizeUrl('GOOGLE', { redirectUrl });
window.location.href = response.url; // 'url' doesn't exist!
// ✅ CORRECT
const response = await omni.getOAuthAuthorizeUrl('GOOGLE', { redirectUrl });
window.location.href = response.authorizationUrl; // Correct property name
`
`typescript
// ❌ WRONG - creating your own type
type Provider = 'google' | 'facebook'; // lowercase won't work!
// ✅ CORRECT - import from SDK
import { CustomerOAuthProvider } from 'omni-sync-sdk';
// CustomerOAuthProvider = 'GOOGLE' | 'FACEBOOK' | 'GITHUB' (UPPERCASE)
const provider: CustomerOAuthProvider = 'GOOGLE';
await omni.getOAuthAuthorizeUrl(provider, { redirectUrl });
`
`typescript
// ❌ WRONG - expecting array directly
const providers = await omni.getAvailableOAuthProviders();
providers.forEach(p => ...); // Error! providers is not an array
// ✅ CORRECT - access the providers property
const response = await omni.getAvailableOAuthProviders();
response.providers.forEach(p => ...); // response.providers is the array
`
Optional fields in SDK types use null, not undefined:
`typescript
// SDK types use:
slug: string | null;
salePrice: string | null;
// So when checking:
if (product.slug !== null) {
// ✅ Check for null
// ...
}
`
`typescript
// ❌ WRONG - these fields don't exist on Cart
const total = cart.total; // ← 'total' doesn't exist!
const discount = cart.discount; // ← 'discount' doesn't exist! It's 'discountAmount'
// ✅ CORRECT - use the helper function (RECOMMENDED)
import { getCartTotals } from 'omni-sync-sdk';
const totals = getCartTotals(cart, shippingPrice);
// Returns: { subtotal: 59.98, discount: 10, shipping: 5.99, total: 55.97 }
// ✅ CORRECT - or calculate manually
const subtotal = parseFloat(cart.subtotal);
const discount = parseFloat(cart.discountAmount); // ← Note: 'discountAmount', NOT 'discount'
const total = subtotal - discount;
`
Important Notes:
- Cart field is discountAmount, NOT discounttotal
- Cart has NO field - use getCartTotals() or calculatetotal
- Checkout DOES have a field, but Cart does notgetCartTotals()
- only works with server Cart (which has subtotal and discountAmount fields). It does NOT work with LocalCart (guest cart from localStorage). For LocalCart, calculate totals manually:`
typescript`
const subtotal = cart.items.reduce(
(sum, item) => sum + parseFloat(item.price || '0') * item.quantity,
0
);
`typescript
// In SearchSuggestions, ProductSuggestion has:
// - price: effective price (sale price if on sale, otherwise base price)
// - basePrice: original price
// - salePrice: sale price if on sale
// ✅ Use 'price' for display (it's already the correct price)
suggestions.products.map(p => (
$3
This causes "ghost items" in the cart after successful payment!
`typescript
// ❌ WRONG - Cart items remain after payment!
// In your success page:
export default function SuccessPage() {
return Thank you for your order!;
// User goes back to shop → still sees purchased items in cart!
}// ✅ CORRECT - Call completeGuestCheckout() on success page
export default function SuccessPage() {
const checkoutId = new URLSearchParams(window.location.search).get('checkout_id');
useEffect(() => {
if (checkoutId) {
// Send order to server AND clear cart
omni.completeGuestCheckout(checkoutId);
}
}, []);
return
Thank you for your order!;
}
`Why is this needed?
-
completeGuestCheckout() sends the order to the server AND clears the local cart
- Without it, the order is never created on the server (payment goes through but no order!)
- For partial checkout (AliExpress-style), only the purchased items are removed
- WARNING: Do NOT use handlePaymentSuccess() - it only clears localStorage and does NOT create the order---
Checkout: Guest vs Logged-In Customer
> ⚠️ CRITICAL: There are TWO different checkout flows. Using the wrong one will cause errors!
| Customer Type | Cart Type | With Payment (Stripe) | Without Payment UI |
| ------------- | ------------------------- | ---------------------- | -------------------- |
| Guest | Local Cart (localStorage) |
startGuestCheckout() | submitGuestOrder() |
| Logged In | Server Cart | createCheckout() | completeCheckout() |$3
`typescript
// ❌ WRONG - This will FAIL with "Cart not found" error!
const cart = omni.getLocalCart(); // Returns cart with id: "__local__"
const checkout = await omni.createCheckout({ cartId: cart.id }); // ERROR!// The "__local__" ID is virtual - it doesn't exist on the server!
`$3
`typescript
// ✅ CORRECT - Use startGuestCheckout() for guests with local cart
const result = await omni.startGuestCheckout();if (result.tracked) {
// Now you have a REAL checkout on the server
const checkout = await omni.getCheckout(result.checkoutId);
// Continue with shipping, payment, etc.
await omni.setShippingAddress(result.checkoutId, { ... });
const intent = await omni.createPaymentIntent(result.checkoutId);
// ... Stripe payment ...
}
`$3
`typescript
// ALWAYS check this at checkout!
if (isLoggedIn()) {
// ✅ Logged-in customer → Server Cart + Checkout flow
// Orders will be linked to their account
const order = await completeServerCheckout();
} else {
// ✅ Guest → Local Cart + submitGuestOrder
// Orders are standalone (not linked to any account)
const order = await omni.submitGuestOrder();
}
`$3
`typescript
// Cart stored locally - NO API calls until checkout!// Add to local cart (stored in localStorage)
omni.addToLocalCart({
productId: products[0].id,
quantity: 1,
name: products[0].name,
price: String(products[0].basePrice),
});
// Set customer info
omni.setLocalCartCustomer({ email: 'customer@example.com' });
omni.setLocalCartShippingAddress({
firstName: 'John',
lastName: 'Doe',
line1: '123 Main St',
city: 'New York',
postalCode: '10001',
country: 'US',
});
// Submit order (single API call!)
const order = await omni.submitGuestOrder();
console.log('Order created:', order.orderId);
`$3
`typescript
// 1. Make sure customer token is set (after login)
omni.setCustomerToken(authResponse.token);// 2. Create server cart (auto-linked to customer!)
const cart = await omni.createCart();
localStorage.setItem('cartId', cart.id);
// 3. Add items to server cart
await omni.addToCart(cart.id, {
productId: products[0].id,
quantity: 1,
});
// 4. Create checkout from cart
const checkout = await omni.createCheckout({ cartId: cart.id });
// 5. Set customer info (REQUIRED - email is needed for order!)
await omni.setCheckoutCustomer(checkout.id, {
email: 'customer@example.com',
firstName: 'John',
lastName: 'Doe',
});
// 6. Set shipping address
await omni.setShippingAddress(checkout.id, {
firstName: 'John',
lastName: 'Doe',
line1: '123 Main St',
city: 'New York',
postalCode: '10001',
country: 'US',
});
// 7. Get shipping rates and select one
const rates = await omni.getShippingRates(checkout.id);
await omni.selectShippingMethod(checkout.id, rates[0].id);
// 8. Complete checkout - order is linked to customer!
const { orderId } = await omni.completeCheckout(checkout.id);
console.log('Order created:', orderId);
// Customer can now see this order in omni.getMyOrders()
`> WARNING: Do NOT use
submitGuestOrder() for logged-in customers! Their orders won't be linked to their account and won't appear in their order history.---
Two Ways to Handle Cart
$3
For guest users, the cart is stored in localStorage - exactly like Amazon, Shopify, and other major platforms do. This means:
- ✅ No API calls when browsing/adding to cart
- ✅ Cart persists across page refreshes
- ✅ Single API call at checkout
- ✅ No server load for window shoppers
`typescript
// Add product to local cart
omni.addToLocalCart({ productId: 'prod_123', quantity: 2 });// View cart
const cart = omni.getLocalCart();
console.log('Items:', cart.items.length);
// Update quantity
omni.updateLocalCartItem('prod_123', 5);
// Remove item
omni.removeFromLocalCart('prod_123');
// At checkout - submit everything in ONE API call
const order = await omni.submitGuestOrder();
`$3
For logged-in customers, you MUST use server-side cart to link orders to their account:
- ✅ Cart syncs across devices
- ✅ Abandoned cart recovery
- ✅ Orders linked to customer account
- ✅ Customer can see orders in "My Orders"
`typescript
// 1. Set customer token (after login)
omni.setCustomerToken(token);// 2. Create cart (auto-linked to customer)
const cart = await omni.createCart();
localStorage.setItem('cartId', cart.id);
// 3. Add items
await omni.addToCart(cart.id, { productId: 'prod_123', quantity: 2 });
// 4. At checkout - create checkout and complete
const checkout = await omni.createCheckout({ cartId: cart.id });
// ... set shipping address, select shipping method ...
const { orderId } = await omni.completeCheckout(checkout.id);
`> ⚠️ CRITICAL: If you use
submitGuestOrder() for a logged-in customer, their order will NOT be linked to their account!---
Complete Store Setup
$3
Create a file
lib/omni-sync.ts:`typescript
import { OmniSyncClient } from 'omni-sync-sdk';export const omni = new OmniSyncClient({
connectionId: 'vc_YOUR_CONNECTION_ID', // Your Connection ID from OmniSync
});
// ----- Guest Cart Helpers (localStorage) -----
export function getCartItemCount(): number {
return omni.getLocalCartItemCount();
}
export function getCart() {
return omni.getLocalCart();
}
// ----- For Registered Users (server cart) -----
export function getServerCartId(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('cartId');
}
export function setServerCartId(id: string): void {
localStorage.setItem('cartId', id);
}
export function clearServerCartId(): void {
localStorage.removeItem('cartId');
}
// ----- Customer Token Helpers -----
export function setCustomerToken(token: string | null): void {
if (token) {
localStorage.setItem('customerToken', token);
omni.setCustomerToken(token);
} else {
localStorage.removeItem('customerToken');
omni.clearCustomerToken();
}
}
export function restoreCustomerToken(): string | null {
const token = localStorage.getItem('customerToken');
if (token) omni.setCustomerToken(token);
return token;
}
export function isLoggedIn(): boolean {
return !!localStorage.getItem('customerToken');
}
`---
Important: Cart & Checkout Data Structures
$3
Cart and Checkout items use a nested structure for product and variant data. This is a common pattern that prevents data duplication and ensures consistency.
Common Mistake:
`typescript
// WRONG - product name is NOT at top level
const name = item.name; // undefined!
const sku = item.sku; // undefined!
`Correct Access Pattern:
`typescript
// CORRECT - access via nested objects
const name = item.product.name;
const sku = item.product.sku;
const variantName = item.variant?.name;
const variantSku = item.variant?.sku;
`$3
| What You Want | CartItem | CheckoutLineItem |
| -------------- | ------------------------- | ------------------------- |
| Product Name |
item.product.name | item.product.name |
| Product SKU | item.product.sku | item.product.sku |
| Product ID | item.productId | item.productId |
| Product Images | item.product.images | item.product.images |
| Variant Name | item.variant?.name | item.variant?.name |
| Variant SKU | item.variant?.sku | item.variant?.sku |
| Variant ID | item.variantId | item.variantId |
| Unit Price | item.unitPrice (string) | item.unitPrice (string) |
| Quantity | item.quantity | item.quantity |$3
All monetary values in Cart and Checkout are returned as strings (e.g.,
"29.99") to preserve decimal precision across different systems. Use parseFloat() or the formatPrice() helper:`typescript
// Monetary fields that are strings:
// - CartItem: unitPrice, discountAmount
// - Cart: subtotal, discountAmount
// - CheckoutLineItem: unitPrice, discountAmount
// - Checkout: subtotal, discountAmount, shippingAmount, taxAmount, total
// - ShippingRate: priceimport { formatPrice } from 'omni-sync-sdk';
// Option 1: Using formatPrice helper (recommended)
const cart = await omni.getCart(cartId);
const total = formatPrice(cart.subtotal); // "$59.98"
const totalNum = formatPrice(cart.subtotal, { asNumber: true }); // 59.98
// Option 2: Manual parseFloat
const subtotal = parseFloat(cart.subtotal);
const discount = parseFloat(cart.discountAmount);
const total = subtotal - discount;
// Line item total
cart.items.forEach((item) => {
const lineTotal = parseFloat(item.unitPrice) * item.quantity;
console.log(
${item.product.name}: $${lineTotal.toFixed(2)});
});
`$3
`typescript
import type { CartItem } from 'omni-sync-sdk';
import { formatPrice } from 'omni-sync-sdk';function CartItemRow({ item }: { item: CartItem }) {
// Access nested product data
const productName = item.product.name;
const productSku = item.product.sku;
const productImage = item.product.images?.[0]?.url;
// Access nested variant data (if exists)
const variantName = item.variant?.name;
const displayName = variantName ?
${productName} - ${variantName} : productName; // Format price using helper
const unitPrice = formatPrice(item.unitPrice);
const lineTotal = formatPrice(item.unitPrice, { asNumber: true }) * item.quantity;
return (

{displayName}
SKU: {item.variant?.sku || productSku}
Qty: {item.quantity}
${lineTotal.toFixed(2)}
);
}
`---
API Reference
$3
#### Get Products (with pagination)
`typescript
import { omni } from '@/lib/omni-sync';
import type { Product, PaginatedResponse } from 'omni-sync-sdk';const response: PaginatedResponse = await omni.getProducts({
page: 1,
limit: 12,
search: 'shirt', // Optional: search by name
status: 'active', // Optional: 'active' | 'draft' | 'archived'
type: 'SIMPLE', // Optional: 'SIMPLE' | 'VARIABLE'
sortBy: 'createdAt', // Optional: 'name' | 'createdAt' | 'updatedAt' | 'basePrice'
sortOrder: 'desc', // Optional: 'asc' | 'desc'
});
console.log(response.data); // Product[]
console.log(response.meta.total); // Total number of products
console.log(response.meta.totalPages); // Total pages
`#### Get Single Product
`typescript
const product: Product = await omni.getProduct('product_id');console.log(product.name);
console.log(product.basePrice);
console.log(product.salePrice); // null if no sale
console.log(product.images); // ProductImage[]
console.log(product.variants); // ProductVariant[] (for VARIABLE products)
console.log(product.inventory); // { total, reserved, available }
`#### Search Suggestions (Autocomplete)
Get search suggestions for building autocomplete/search-as-you-type UI:
`typescript
import type { SearchSuggestions } from 'omni-sync-sdk';// Basic autocomplete
const suggestions: SearchSuggestions = await omni.getSearchSuggestions('shirt');
console.log(suggestions.products);
// [{ id, name, image, basePrice, salePrice, type }]
console.log(suggestions.categories);
// [{ id, name, productCount }]
// With custom limit (default: 5, max: 10)
const suggestions = await omni.getSearchSuggestions('dress', 3);
`Search covers: name, sku, description, categories, tags, and brands.
Example: Search Input with Suggestions
`typescript
function SearchInput() {
const [query, setQuery] = useState('');
const [suggestions, setSuggestions] = useState(null); // Debounce search requests
useEffect(() => {
if (query.length < 2) {
setSuggestions(null);
return;
}
const timer = setTimeout(async () => {
const results = await omni.getSearchSuggestions(query, 5);
setSuggestions(results);
}, 300);
return () => clearTimeout(timer);
}, [query]);
return (
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
/>
{suggestions && (
{suggestions.products.map((product) => (
/products/${product.slug}}>

{product.name}
${product.basePrice}
))}
{suggestions.categories.map((category) => (
/category/${category.id}}>
{category.name} ({category.productCount} products)
))}
)}
);
}
`#### Product Type Definition
`typescript
interface Product {
id: string;
name: string;
description?: string | null;
descriptionFormat?: 'text' | 'html' | 'markdown'; // Format of description content
sku: string;
basePrice: number;
salePrice?: number | null;
status: 'active' | 'draft' | 'archived';
type: 'SIMPLE' | 'VARIABLE';
images?: ProductImage[];
inventory?: InventoryInfo | null;
variants?: ProductVariant[];
categories?: string[];
tags?: string[];
createdAt: string;
updatedAt: string;
}interface ProductImage {
url: string;
position?: number;
isMain?: boolean;
}
interface ProductVariant {
id: string;
sku?: string | null;
name?: string | null;
price?: number | null;
salePrice?: number | null;
attributes?: Record;
inventory?: InventoryInfo | null;
}
interface InventoryInfo {
total: number;
reserved: number;
available: number;
trackingMode?: 'TRACKED' | 'UNLIMITED' | 'DISABLED';
inStock: boolean; // Pre-calculated - use this for display!
canPurchase: boolean; // Pre-calculated - use this for add-to-cart
}
`#### Product Metafields (Custom Fields)
Products can have custom fields (metafields) defined by the store owner, such as "Material", "Care Instructions", or "Warranty".
`typescript
import {
getProductMetafield,
getProductMetafieldValue,
getProductMetafieldsByType,
} from 'omni-sync-sdk';const product = await omni.getProductBySlug('blue-shirt');
// Access all metafields
product.metafields?.forEach((field) => {
console.log(
${field.definitionName}: ${field.value});
});// Get a specific metafield by key
const material = getProductMetafieldValue(product, 'material'); // auto-parsed (string | number | boolean | null)
const careField = getProductMetafield(product, 'care_instructions'); // full ProductMetafield object
// Filter metafields by type
const textFields = getProductMetafieldsByType(product, 'TEXT');
// Fetch metafield definitions (schema) to build dynamic UI
const { definitions } = await omni.getPublicMetafieldDefinitions();
definitions.forEach((def) => {
console.log(
${def.name} (${def.key}): ${def.type}, required: ${def.required});
});
`> Note:
metafields may be empty if the store hasn't defined custom fields. Always use optional chaining (product.metafields?.forEach).#### Displaying Price Range for Variable Products
For products with
type: 'VARIABLE' and multiple variants with different prices, display a price range instead of a single price:`typescript
// Helper function to get price range from variants
function getPriceRange(product: Product): { min: number; max: number } | null {
if (product.type !== 'VARIABLE' || !product.variants?.length) {
return null;
} const prices = product.variants
.map(v => v.price ?? product.basePrice)
.filter((p): p is number => p !== null);
if (prices.length === 0) return null;
const min = Math.min(...prices);
const max = Math.max(...prices);
// Return null if all variants have the same price
return min !== max ? { min, max } : null;
}
// Usage in component
function ProductPrice({ product }: { product: Product }) {
const priceRange = getPriceRange(product);
if (priceRange) {
// Variable product with different variant prices - show range
return ${priceRange.min} - ${priceRange.max};
}
// Simple product or all variants same price - show single price
return product.salePrice ? (
<>
${product.salePrice}
${product.basePrice}
>
) : (
${product.basePrice}
);
}
`When to show price range:
- Product
type is 'VARIABLE'
- Has 2+ variants with different prices
- Example: T-shirt sizes S/M/L at $29, XL/XXL at $34 → Display "$29 - $34"When to show single price:
- Product
type is 'SIMPLE'
- Variable product where all variants have the same price#### Rendering Product Descriptions
> CRITICAL: Product descriptions from Shopify/WooCommerce contain HTML tags. If you render them as plain text, users will see raw
,
, tags instead of formatted content!Use the SDK helper functions to handle this automatically:
`tsx
import { isHtmlDescription, getDescriptionContent } from 'omni-sync-sdk';// Option 1: Using isHtmlDescription helper (recommended)
function ProductDescription({ product }: { product: Product }) {
if (!product.description) return null;
if (isHtmlDescription(product)) {
// HTML from Shopify/WooCommerce - MUST use dangerouslySetInnerHTML
return
;
} // Plain text - render normally
return
{product.description}
;
}// Option 2: Using getDescriptionContent helper
function ProductDescription({ product }: { product: Product }) {
const content = getDescriptionContent(product);
if (!content) return null;
if ('html' in content) {
return
;
} return
{content.text}
;
}
`| Source Platform | descriptionFormat | Rendering |
| --------------- | ----------------- | ----------------------------- |
| Shopify |
'html' | Use dangerouslySetInnerHTML |
| WooCommerce | 'html' | Use dangerouslySetInnerHTML |
| TikTok | 'text' | Render as plain text |
| Manual entry | 'text' | Render as plain text |Common Mistake - DO NOT do this:
`tsx
// WRONG - HTML will show as raw tags like Hello
{product.description}
`---
$3
The local cart stores everything in localStorage until checkout. This is the recommended approach for most storefronts.
#### Add to Local Cart
`typescript
// Add item with product info (for display)
omni.addToLocalCart({
productId: 'prod_123',
variantId: 'var_456', // Optional: for products with variants
quantity: 2,
name: 'Cool T-Shirt', // Optional: for cart display
price: '29.99', // Optional: for cart display
image: 'https://...', // Optional: for cart display
});
`#### Get Local Cart
`typescript
const cart = omni.getLocalCart();console.log(cart.items); // Array of cart items
console.log(cart.customer); // Customer info (if set)
console.log(cart.shippingAddress); // Shipping address (if set)
console.log(cart.couponCode); // Applied coupon (if any)
`#### Update Item Quantity
`typescript
// Set quantity to 5
omni.updateLocalCartItem('prod_123', 5);// For variant products
omni.updateLocalCartItem('prod_123', 3, 'var_456');
// Set to 0 to remove
omni.updateLocalCartItem('prod_123', 0);
`#### Remove Item
`typescript
omni.removeFromLocalCart('prod_123');
omni.removeFromLocalCart('prod_123', 'var_456'); // With variant
`#### Clear Cart
`typescript
omni.clearLocalCart();
`#### Set Customer Info
`typescript
omni.setLocalCartCustomer({
email: 'customer@example.com', // Required
firstName: 'John', // Optional
lastName: 'Doe', // Optional
phone: '+1234567890', // Optional
});
`#### Set Shipping Address
`typescript
omni.setLocalCartShippingAddress({
firstName: 'John',
lastName: 'Doe',
line1: '123 Main St',
line2: 'Apt 4B', // Optional
city: 'New York',
region: 'NY', // Optional: State/Province
postalCode: '10001',
country: 'US',
phone: '+1234567890', // Optional
});
`#### Set Billing Address (Optional)
`typescript
omni.setLocalCartBillingAddress({
firstName: 'John',
lastName: 'Doe',
line1: '456 Business Ave',
city: 'New York',
postalCode: '10002',
country: 'US',
});
`#### Apply Coupon
`typescript
omni.setLocalCartCoupon('SAVE20');// Remove coupon
omni.setLocalCartCoupon(undefined);
`#### Get Cart Item Count
`typescript
const count = omni.getLocalCartItemCount();
console.log(${count} items in cart);
`#### Local Cart Type Definition
`typescript
interface LocalCart {
items: LocalCartItem[];
couponCode?: string;
customer?: {
email: string;
firstName?: string;
lastName?: string;
phone?: string;
};
shippingAddress?: {
firstName: string;
lastName: string;
line1: string;
line2?: string;
city: string;
region?: string;
postalCode: string;
country: string;
phone?: string;
};
billingAddress?: {
/ same as shipping /
};
notes?: string;
updatedAt: string;
}interface LocalCartItem {
productId: string;
variantId?: string;
quantity: number;
name?: string;
sku?: string;
price?: string;
image?: string;
addedAt: string;
}
`---
$3
Submit the local cart as an order with a single API call:
`typescript
// Make sure cart has items, customer email, and shipping address
const order = await omni.submitGuestOrder();console.log(order.orderId); // 'order_abc123...'
console.log(order.orderNumber); // 'ORD-12345'
console.log(order.status); // 'pending'
console.log(order.total); // 59.98
console.log(order.message); // 'Order created successfully'
// Cart is automatically cleared after successful order
`> 🔄 Automatic Tracking: If "Track Guest Checkouts" is enabled in your connection settings (OmniSync Admin),
submitGuestOrder() will automatically create a tracked checkout session before placing the order. This allows you to see abandoned carts and checkout sessions in your admin dashboard - no code changes needed!#### Keep Cart After Order
`typescript
// If you want to keep the cart data (e.g., for order review page)
const order = await omni.submitGuestOrder({ clearCartOnSuccess: false });
`#### Create Order with Custom Data
If you manage cart state yourself instead of using local cart:
`typescript
const order = await omni.createGuestOrder({
items: [
{ productId: 'prod_123', quantity: 2 },
{ productId: 'prod_456', variantId: 'var_789', quantity: 1 },
],
customer: {
email: 'customer@example.com',
firstName: 'John',
lastName: 'Doe',
},
shippingAddress: {
firstName: 'John',
lastName: 'Doe',
line1: '123 Main St',
city: 'New York',
postalCode: '10001',
country: 'US',
},
couponCode: 'SAVE20', // Optional
notes: 'Please gift wrap', // Optional
});
`#### Guest Order Response Type
`typescript
interface GuestOrderResponse {
orderId: string;
orderNumber: string;
status: string;
total: number;
message: string;
}
`---
$3
> Note: As of SDK v0.7.1,
submitGuestOrder() automatically handles tracking. You don't need to use these methods unless you want explicit control over the checkout flow.When "Track Guest Checkouts" is enabled in your connection settings, checkout sessions are automatically created on the server, allowing:
- Visibility of checkout sessions in admin dashboard
- Abandoned cart tracking
- Future: abandoned cart recovery emails
#### How to Enable
1. Go to OmniSync Admin → Integrations → Vibe-Coded Sites
2. Click on your connection → Settings
3. Enable "Track Guest Checkouts"
4. Save - that's it! No code changes needed.
#### Advanced: Manual Tracking Control
If you need explicit control over the tracking flow (e.g., to track checkout steps before the user places an order):
`typescript
// 1. Start tracked checkout (sends cart items to server)
const checkout = await omni.startGuestCheckout();if (checkout.tracked) {
// 2. Update with shipping address
await omni.updateGuestCheckoutAddress(checkout.checkoutId, {
shippingAddress: {
firstName: 'John',
lastName: 'Doe',
line1: '123 Main St',
city: 'New York',
postalCode: '10001',
country: 'US',
},
});
// 3. Complete the checkout
const order = await omni.completeGuestCheckout(checkout.checkoutId);
console.log('Order created:', order.orderId);
} else {
// Fallback to regular guest checkout
const order = await omni.submitGuestOrder();
}
`#### Response Types
`typescript
type GuestCheckoutStartResponse =
| {
tracked: true;
checkoutId: string;
cartId: string;
message: string;
}
| {
tracked: false;
message: string;
};
`---
$3
For logged-in customers who want cart sync across devices.
#### Create Cart
`typescript
const cart = await omni.createCart();
setServerCartId(cart.id); // Save to localStorage
`#### Get Cart
`typescript
const cartId = getCartId();
if (cartId) {
const cart = await omni.getCart(cartId);
console.log(cart.items); // CartItem[]
console.log(cart.itemCount); // Total items
console.log(cart.subtotal); // Subtotal amount
}
`#### Add to Cart
`typescript
const cart = await omni.addToCart(cartId, {
productId: 'product_id',
variantId: 'variant_id', // Optional: for VARIABLE products
quantity: 2,
notes: 'Gift wrap please', // Optional
});
`#### Update Cart Item
`typescript
const cart = await omni.updateCartItem(cartId, itemId, {
quantity: 3,
});
`#### Remove Cart Item
`typescript
const cart = await omni.removeCartItem(cartId, itemId);
`#### Apply Coupon
`typescript
const cart = await omni.applyCoupon(cartId, 'SAVE20');
console.log(cart.discountAmount); // Discount applied
console.log(cart.couponCode); // 'SAVE20'
`#### Remove Coupon
`typescript
const cart = await omni.removeCoupon(cartId);
`#### Cart Type Definition
`typescript
interface Cart {
id: string;
sessionToken?: string | null;
customerId?: string | null;
status: 'ACTIVE' | 'MERGED' | 'CONVERTED' | 'ABANDONED';
currency: string;
subtotal: string;
discountAmount: string;
couponCode?: string | null;
items: CartItem[];
itemCount: number;
createdAt: string;
updatedAt: string;
}interface CartItem {
id: string;
productId: string;
variantId?: string | null;
quantity: number;
unitPrice: string;
discountAmount: string;
notes?: string | null;
product: {
id: string;
name: string;
sku: string;
images?: unknown[];
};
variant?: {
id: string;
name?: string | null;
sku?: string | null;
} | null;
}
`---
$3
#### Create Checkout from Cart
`typescript
const checkout = await omni.createCheckout({
cartId: cartId,
});
`#### Partial Checkout (AliExpress-style)
Allow customers to select which items to checkout from their cart. Only selected items are purchased - remaining items stay in the cart for later.
`typescript
// 1. In your cart page, track selected items
const [selectedItems, setSelectedItems] = useState>(new Set());// 2. Create checkout with only selected items
const checkout = await omni.createCheckout({
cartId: cart.id,
selectedItemIds: Array.from(selectedItems), // Only these items go to checkout
});
// 3. Before checkout, check stock ONLY for selected items
const stockCheck = await omni.checkCartStock(cart, Array.from(selectedItems));
if (!stockCheck.allAvailable) {
// Handle out-of-stock items
}
// After successful payment:
// - Selected items are REMOVED from cart
// - Unselected items REMAIN in cart (cart stays ACTIVE)
// - Customer can continue shopping and checkout remaining items later
`#### Set Customer Information
`typescript
const checkout = await omni.setCheckoutCustomer(checkoutId, {
email: 'customer@example.com',
firstName: 'John',
lastName: 'Doe',
phone: '+1234567890', // Optional
});
`#### Set Shipping Address
`typescript
const { checkout, rates } = await omni.setShippingAddress(checkoutId, {
firstName: 'John',
lastName: 'Doe',
line1: '123 Main St',
line2: 'Apt 4B', // Optional
city: 'New York',
region: 'NY', // State/Province
postalCode: '10001',
country: 'US',
phone: '+1234567890', // Optional
});// rates contains available shipping options
console.log(rates); // ShippingRate[]
`#### Select Shipping Method
`typescript
const checkout = await omni.selectShippingMethod(checkoutId, rates[0].id);
`#### Set Billing Address
`typescript
// Same as shipping
const checkout = await omni.setBillingAddress(checkoutId, {
...shippingAddress,
sameAsShipping: true, // Optional shortcut
});
`#### Complete Checkout
`typescript
const { orderId } = await omni.completeCheckout(checkoutId);
clearCartId(); // Clear cart from localStorage
console.log('Order created:', orderId);
`#### Checkout Type Definition
`typescript
interface Checkout {
id: string;
status: CheckoutStatus;
email?: string | null;
shippingAddress?: CheckoutAddress | null;
billingAddress?: CheckoutAddress | null;
shippingMethod?: ShippingRate | null;
currency: string;
subtotal: string;
discountAmount: string;
shippingAmount: string;
taxAmount: string;
total: string;
couponCode?: string | null;
items: CheckoutLineItem[];
itemCount: number;
availableShippingRates?: ShippingRate[];
}type CheckoutStatus = 'PENDING' | 'SHIPPING_SET' | 'PAYMENT_PENDING' | 'COMPLETED' | 'FAILED';
interface ShippingRate {
id: string;
name: string;
description?: string | null;
price: string;
currency: string;
estimatedDays?: number | null;
}
`#### Shipping Rates: Complete Flow
The shipping flow involves setting an address and then selecting from available rates:
`typescript
// Step 1: Set shipping address - this returns available rates
const { checkout, rates } = await omni.setShippingAddress(checkoutId, {
firstName: 'John',
lastName: 'Doe',
line1: '123 Main St',
city: 'New York',
region: 'NY',
postalCode: '10001',
country: 'US',
});// Step 2: Handle empty rates (edge case)
if (rates.length === 0) {
// No shipping options available for this address
// This can happen when:
// - Store doesn't ship to this address/country
// - All shipping methods have restrictions that exclude this address
// - Shipping rates haven't been configured in the store
return (
No shipping options available
We currently cannot ship to this address. Please try a different address or contact us for
assistance.
);
}// Step 3: Display available rates to customer
Select Shipping Method
{rates.map((rate) => (
))}
;// Step 4: Select the shipping method
await omni.selectShippingMethod(checkoutId, selectedRateId);
`Handling Empty Shipping Rates:
When no shipping rates are available, you have several options:
`typescript
// Option 1: Show helpful message
if (rates.length === 0) {
return ;
}// Option 2: Allow customer to contact store
if (rates.length === 0) {
return (
);
}// Option 3: Validate before proceeding
function canProceedToPayment(checkout: Checkout, rates: ShippingRate[]): boolean {
if (rates.length === 0) return false;
if (!checkout.shippingRateId) return false;
if (!checkout.email) return false;
return true;
}
`---
$3
For vibe-coded sites, the SDK provides payment integration with Stripe and PayPal. The store owner configures their payment provider(s) in the admin, and your site uses these methods to process payments.
#### ⚠️ Important: Getting a Valid Checkout ID
Before creating a payment intent, you need a checkout ID. How you get it depends on the customer type:
`typescript
// For GUEST users (local cart in localStorage):
const result = await omni.startGuestCheckout();
const checkoutId = result.checkoutId;// For LOGGED-IN users (server cart):
const checkout = await omni.createCheckout({ cartId: serverCartId });
const checkoutId = checkout.id;
// Then continue with shipping and payment...
`#### Get All Payment Providers (Recommended)
Use this method to get ALL enabled payment providers and build dynamic UI:
`typescript
const { hasPayments, providers, defaultProvider } = await omni.getPaymentProviders();// Returns:
// {
// hasPayments: true,
// providers: [
// {
// id: 'provider_xxx',
// provider: 'stripe',
// name: 'Stripe',
// publicKey: 'pk_live_xxx...',
// supportedMethods: ['card', 'ideal'],
// testMode: false,
// isDefault: true
// },
// {
// id: 'provider_yyy',
// provider: 'paypal',
// name: 'PayPal',
// publicKey: 'client_id_xxx...',
// supportedMethods: ['paypal'],
// testMode: false,
// isDefault: false
// }
// ],
// defaultProvider: { ... } // The default provider (first one)
// }
// Build dynamic UI based on available providers
if (!hasPayments) {
return
Payment not configured for this store;
}const stripeProvider = providers.find(p => p.provider === 'stripe');
const paypalProvider = providers.find(p => p.provider === 'paypal');
// Show Stripe payment form if available
if (stripeProvider) {
const stripe = await loadStripe(stripeProvider.publicKey);
// ... show Stripe Elements
}
// Show PayPal buttons if available
if (paypalProvider) {
// ... show PayPal buttons with paypalProvider.publicKey as client-id
}
`#### Get Payment Configuration (Single Provider)
If you only need the default provider, use this simpler method:
`typescript
const config = await omni.getPaymentConfig();// Returns:
// {
// provider: 'stripe' | 'paypal',
// publicKey: 'pk_live_xxx...', // Stripe publishable key or PayPal client ID
// supportedMethods: ['card', 'ideal', 'bancontact'],
// testMode: false
// }
`#### Create Payment Intent
After the customer fills in shipping details, create a payment intent:
`typescript
const intent = await omni.createPaymentIntent(checkout.id);// Returns:
// {
// id: 'pi_xxx...',
// clientSecret: 'pi_xxx_secret_xxx', // Used by Stripe.js/PayPal SDK
// amount: 9999, // In cents
// currency: 'USD',
// status: 'requires_payment_method'
// }
`#### Confirm Payment with Stripe.js
Use the client secret with Stripe.js to collect payment:
`typescript
// Initialize Stripe.js with the public key from getPaymentConfig()
const stripe = await loadStripe(config.publicKey);// Create Elements and Payment Element
const elements = stripe.elements({ clientSecret: intent.clientSecret });
const paymentElement = elements.create('payment');
paymentElement.mount('#payment-element');
// When customer submits payment
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url:
${window.location.origin}/checkout/success?checkout_id=${checkout.id},
},
});if (error) {
console.error('Payment failed:', error.message);
}
`#### After Payment: Success Page Pattern (Recommended)
Important: Orders are created asynchronously via webhook after Stripe confirms payment.
This typically takes 1-5 seconds, but can vary. Follow these best practices:
Option 1: Optimistic Success Page (Recommended - Used by Amazon, Shopify, AliExpress)
Show success immediately without waiting for orderId. This is the industry standard:
`typescript
// In your payment form, after stripe.confirmPayment() succeeds:
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: ${window.location.origin}/checkout/success?checkout_id=${checkout.id},
},
});// On /checkout/success page - show confirmation IMMEDIATELY:
export default function CheckoutSuccessPage() {
const checkoutId = new URLSearchParams(window.location.search).get('checkout_id');
return (
Payment Received!
Your order is being processed.
Confirmation #{checkoutId?.slice(-8).toUpperCase()}
A confirmation email will be sent shortly.
View Your Orders →
);
}
`Option 2: Wait for Order (For SPAs that need orderId)
Use
waitForOrder() to poll in the background with exponential backoff:`typescript
// After payment succeeds, wait for order creation (max 30 seconds)
const result = await omni.waitForOrder(checkout.id, {
maxWaitMs: 30000, // 30 seconds max
onPollAttempt: (attempt, status) => {
console.log(Checking order status... (attempt ${attempt}));
},
onOrderReady: (status) => {
// Called immediately when order is created
console.log('Order ready:', status.orderNumber);
},
});if (result.success) {
// Order was created within timeout
window.location.href =
/orders/${result.status.orderId};
} else {
// Order not created yet - show optimistic success anyway
// The email will be sent when order is ready
showSuccessMessage('Payment received! Order confirmation coming soon.');
}
`Option 3: Simple Status Check (Single poll)
For simple use cases where you just want to check once:
`typescript
const status = await omni.getPaymentStatus(checkout.id);// Returns:
// {
// checkoutId: 'checkout_xxx',
// status: 'succeeded' | 'pending' | 'failed' | 'canceled',
// orderId: 'order_xxx', // Only if order was created
// orderNumber: 'ORD-123', // Only if order was created
// error: 'Payment declined' // Only if payment failed
// }
if (status.status === 'succeeded' && status.orderId) {
window.location.href =
/order-confirmation/${status.orderId};
} else if (status.status === 'succeeded') {
// Payment succeeded but order not created yet
showMessage('Payment received, processing your order...');
} else if (status.status === 'failed') {
showError(status.error || 'Payment failed');
}
`> Why Optimistic Success? Stripe webhooks typically arrive within 1-5 seconds,
> but network issues can cause delays. Major e-commerce platforms (Amazon, Shopify)
> show success immediately and send order details via email. This provides better UX
> than making customers wait on a loading screen.
#### Complete Checkout with Payment Example
> Note: This example assumes you already have a
checkout_id. See below for how to create one.How to get a checkout_id:
`typescript
// For GUEST users (local cart):
const result = await omni.startGuestCheckout();
const checkoutId = result.checkoutId; // Use this!// For LOGGED-IN users (server cart):
const checkout = await omni.createCheckout({ cartId: cart.id });
const checkoutId = checkout.id; // Use this!
``typescript
'use client';
import { useState, useEffect } from 'react';
import { loadStripe, Stripe, StripeElements } from '@stripe/stripe-js';
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
import { omni } from '@/lib/omni-sync';
import type { Checkout, PaymentConfig, PaymentIntent } from 'omni-sync-sdk';export default function CheckoutPaymentPage() {
const [checkout, setCheckout] = useState(null);
const [paymentConfig, setPaymentConfig] = useState(null);
const [paymentIntent, setPaymentIntent] = useState(null);
const [stripePromise, setStripePromise] = useState | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
async function initPayment() {
try {
// Get checkout_id from URL (set by previous step)
const checkoutId = new URLSearchParams(window.location.search).get('checkout_id');
if (!checkoutId) throw new Error('No checkout ID');
// Get payment configuration
const config = await omni.getPaymentConfig();
setPaymentConfig(config);
// Initialize Stripe
if (config.provider === 'stripe') {
setStripePromise(loadStripe(config.publicKey));
}
// Create payment intent
const intent = await omni.createPaymentIntent(checkoutId);
setPaymentIntent(intent);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to initialize payment');
} finally {
setLoading(false);
}
}
initPayment();
}, []);
if (loading) return
Loading payment...;
if (error) return {error};
if (!paymentConfig || !paymentIntent || !stripePromise) return null; return (
Payment
);
}function PaymentForm({ checkoutId }: { checkoutId: string }) {
const stripe = useStripe();
const elements = useElements();
const [processing, setProcessing] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!stripe || !elements) return;
setProcessing(true);
setError('');
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url:
${window.location.origin}/checkout/success?checkout_id=${checkoutId},
},
}); if (error) {
setError(error.message || 'Payment failed');
setProcessing(false);
}
// If successful, Stripe redirects to return_url
};
return (
);
}
`#### Complete Order After Payment:
completeGuestCheckout()CRITICAL: After payment succeeds, you MUST call
completeGuestCheckout() to create the order on the server and clear the cart.> WARNING: Do NOT use
handlePaymentSuccess() - it only clears the local cart (localStorage)
> and does NOT send the order to the server. Your customer will pay but no order will be created!`typescript
// On your /checkout/success page:
export default function CheckoutSuccessPage() {
const checkoutId = new URLSearchParams(window.location.search).get('checkout_id'); useEffect(() => {
if (checkoutId) {
// IMPORTANT: This sends the order to the server AND clears the cart
// completeGuestCheckout() returns GuestOrderResponse (has .orderNumber directly)
// This is different from waitForOrder() which returns WaitForOrderResult
// (access orderNumber via .status.orderNumber instead)
omni.completeGuestCheckout(checkoutId).then(result => {
console.log('Order created:', result.orderNumber);
}).catch(() => {
// Order may already exist (e.g., page refresh) - safe to ignore
});
}
}, []);
return (
Payment Received!
{/ ... rest of success page /}
);
}
`How it works:
| User Type | Cart Type | Behavior |
|-----------|-----------|----------|
| Guest (partial checkout) | Local cart | Creates order + removes only purchased items |
| Guest (full checkout) | Local cart | Creates order + clears entire cart |
| Logged-in | Server cart | Creates order + clears cart via SDK state |
Why is this needed?
-
completeGuestCheckout() sends POST /checkout/:id/complete which creates the order on the server
- Without it, payment goes through Stripe but no order is created in the system
- The server also links the order to the customer (by email) so it appears in their order history
- For partial checkout (AliExpress-style), only the purchased items are removed---
$3
#### Register Customer
`typescript
const auth = await omni.registerCustomer({
email: 'customer@example.com',
password: 'securepassword123',
firstName: 'John',
lastName: 'Doe',
});// Check if email verification is required
if (auth.requiresVerification) {
localStorage.setItem('verificationToken', auth.token);
window.location.href = '/verify-email';
} else {
setCustomerToken(auth.token);
// Redirect back to store, not /account
window.location.href = '/';
}
`#### Login Customer
`typescript
const auth = await omni.loginCustomer('customer@example.com', 'password123');
setCustomerToken(auth.token);// Best practice: redirect back to previous page or home
const returnUrl = localStorage.getItem('returnUrl') || '/';
localStorage.removeItem('returnUrl');
window.location.href = returnUrl;
`> Best Practice: Before showing login page, save the current URL with
localStorage.setItem('returnUrl', window.location.pathname). After login, redirect back to that URL. This is how Amazon, Shopify, and most e-commerce sites work.#### Logout Customer
`typescript
setCustomerToken(null);
window.location.href = '/'; // Return to store home
`#### Get Customer Profile
`typescript
restoreCustomerToken(); // Restore from localStorage
const profile = await omni.getMyProfile();console.log(profile.firstName);
console.log(profile.email);
console.log(profile.addresses);
`#### Get Customer Orders
`typescript
const { data: orders, meta } = await omni.getMyOrders({
page: 1,
limit: 10,
});
`#### Auth Response Type
`typescript
interface CustomerAuthResponse {
customer: {
id: string;
email: string;
firstName?: string;
lastName?: string;
emailVerified: boolean;
};
token: string;
expiresAt: string;
requiresVerification?: boolean; // true if email verification is required
}
`---
$3
If the store has email verification enabled, customers must verify their email after registration before they can fully use their account.
#### Registration with Email Verification
When
requiresVerification is true in the registration response, the customer needs to verify their email:`typescript
const auth = await omni.registerCustomer({
email: 'customer@example.com',
password: 'securepassword123',
firstName: 'John',
});if (auth.requiresVerification) {
// Save token for verification step
localStorage.setItem('verificationToken', auth.token);
// Redirect to verification page
window.location.href = '/verify-email';
} else {
// No verification needed - redirect back to store
setCustomerToken(auth.token);
window.location.href = '/';
}
`#### Verify Email with Code
After the customer receives the 6-digit code via email:
`typescript
// Get the token saved from registration
const token = localStorage.getItem('verificationToken');// Verify email - pass the token directly (no need to call setCustomerToken first!)
const result = await omni.verifyEmail(code, token);
if (result.verified) {
// Email verified! Now set the token for normal use
setCustomerToken(token);
localStorage.removeItem('verificationToken');
// Redirect back to store (or returnUrl if saved)
const returnUrl = localStorage.getItem('returnUrl') || '/';
localStorage.removeItem('returnUrl');
window.location.href = returnUrl;
}
``#### Resend Ver