brainerce
v1.23.13
Published
Official SDK for building e-commerce storefronts with Brainerce Platform. Perfect for vibe-coded sites, AI-built stores (Cursor, Lovable, v0), and custom storefronts.
Downloads
1,402
Maintainers
Readme
brainerce
Official SDK for building e-commerce storefronts with Brainerce Platform.
This SDK provides a complete solution for vibe-coded sites, AI-built stores (Cursor, Lovable, v0), and custom storefronts to connect to Brainerce's unified commerce API.
AI Agents / Vibe Coders: Use the MCP server for AI-powered store building:
npx @brainerce/mcp-server. It provides docs, code templates, and live store capabilities to any AI tool (Cursor, Lovable, Claude Code).
Installation
npm install brainerce
# or
pnpm add brainerce
# or
yarn add brainerceQuick Reference - Helper Functions
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) |
| isAllowedPaymentUrl(url, options?) | Validate a payment URL host | isAllowedPaymentUrl(intent.clientSecret) → true |
| safePaymentRedirect(url, options?) | Validate then window.location.href | safePaymentRedirect(intent.clientSecret) |
import {
formatPrice,
getDescriptionContent,
getStockStatus,
getProductPrice,
getProductPriceInfo,
getCartTotals,
getCartItemName,
getCartItemImage,
} from 'brainerce';
// 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
brainerce. Never createutils/format.ts,lib/helpers.ts, or similar files - use the SDK exports directly.
⚠️ CRITICAL: Payment Integration Required!
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.
// On your checkout/payment page, ALWAYS call this first:
const { hasPayments, providers } = await client.getPaymentProviders();
if (!hasPayments) {
// Show error - payment is not configured
return <div>Payment not configured for this store</div>;
}
// 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.
Quick Start
For Vibe-Coded Sites (Recommended)
import { BrainerceClient } from 'brainerce';
// Initialize with your Connection ID
const client = new BrainerceClient({
connectionId: 'vc_YOUR_CONNECTION_ID',
});
// Fetch products
const { data: products } = await client.getProducts();Product customization fields (buyer input)
Products can expose customizationFields — merchant-defined inputs the buyer fills on the product page (engraving text, photo upload, select / multi-select options, date pickers, etc.). Render the form from the array, upload any images via uploadCustomizationFile(), then pass values as metadata on add-to-cart. The server validates and snapshots everything onto the order line. Definitions flagged appliesToAllProducts: true are folded into every product's customizationFields automatically — no client-side merging required.
if (product.customizationFields?.length) {
// Render a form control per field using field.type (TEXT, SELECT,
// MULTI_SELECT, IMAGE, GALLERY, DATE, ...) — see INTEGRATION.md §2.8
}
// For IMAGE / GALLERY fields: upload first
const { url: photoUrl } = await client.uploadCustomizationFile(file);
await client.addToCart(cart.id, {
productId: product.id,
quantity: 1,
metadata: {
engraving_text: 'Happy Birthday!',
frame_color: 'Gold', // SELECT — must be in enumValues
upload_photo: photoUrl, // IMAGE — URL from uploadCustomizationFile
addons: ['Gift wrap'], // MULTI_SELECT — always an array
},
});Full rendering guide + per-type validation rules: INTEGRATION.md §2.8 and INTEGRATION-RULES.md.
Modifier groups (restaurant / build-your-own products)
Products can expose modifierGroups — merchant-defined option blocks like "Toppings" (max 8, first 3 free) or "Sauce" (pick exactly one). Render radios for selectionType: 'SINGLE' and checkboxes for 'MULTIPLE', honor defaultModifierIds and isDefault on first render, disable modifiers with available: false, and pass selections on add-to-cart. The server is the source of truth for free-allocation and final pricing.
// 5-line add-to-cart with modifiers
await client.addToCart(cart.id, {
productId: 'prod_pizza',
quantity: 1,
selections: [
{ modifierGroupId: 'mg_bread', modifierIds: ['m_thick'] },
{ modifierGroupId: 'mg_toppings', modifierIds: ['m_olive', 'm_mushroom', 'm_bacon'] },
],
});Money on the wire is always strings (priceDelta: "5.00"). Validation failures arrive as a structured 400 envelope on BrainerceError.details with code: 'MODIFIER_VALIDATION_FAILED' and errors[] — see INTEGRATION-RULES.md "Modifier validation errors" for the full code list.
Full rendering guide: INTEGRATION.md §2.9. Restaurant features (allergens, scheduled availability, nested combos to depth 3, downsell modifiers): INTEGRATION-OPTIONAL.md "Restaurant / build-your-own products".
Common Mistakes to Avoid
AI Agents / Vibe-Coders: Read this section carefully! These are common misunderstandings.
1. Guest Checkout - Use startGuestCheckout() for Guests
For guest users, use startGuestCheckout() which creates a checkout from the session cart:
// ✅ CORRECT - Use startGuestCheckout() for guest users
const result = await client.startGuestCheckout();
if (result.tracked) {
const checkout = await client.getCheckout(result.checkoutId);
// Continue with payment flow...
}
// ✅ ALTERNATIVE - Use submitGuestOrder() for simple checkout without payment UI
const order = await client.submitGuestOrder();Rule of thumb:
- Guest user + Session cart →
startGuestCheckout()orsubmitGuestOrder() - Logged-in user + Server cart →
createCheckout({ cartId })
2. ⛔ NEVER Create Local Interfaces - Use SDK Types!
This causes type errors and runtime bugs!
// ❌ 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 'brainerce';⚠️ SDK Type Facts - Trust These!
| What | Correct | Wrong |
| ------------------------ | ----------------------------- | --------------------- |
| Prices | string (use parseFloat()) | number |
| Cart item name | item.product.name | item.name |
| Order item name | item.name | item.product.name |
| Cart item image | item.product.images[0] | item.image |
| Order item image | item.image | item.product.images |
| Address state/province | region | state or province |
| OAuth redirect URL | authorizationUrl | url |
| OAuth providers response | { providers: [...] } | [...] directly |
If you think a type is "wrong", YOU are wrong. Read the SDK types!
3. formatPrice Expects Options Object
// ❌ WRONG
formatPrice(amount, 'USD');
// ✅ CORRECT
formatPrice(amount, { currency: 'USD' });4. Cart/Checkout vs Order - Different Item Structures!
IMPORTANT: Cart and Checkout items have NESTED product data. Order items are FLAT.
// 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 |
5. Payment Status is 'succeeded', not 'completed'
// ❌ WRONG
if (status.status === 'completed')
// ✅ CORRECT
if (status.status === 'succeeded')6. ProductSuggestion vs Product - Different Types
getSearchSuggestions() returns ProductSuggestion[], NOT Product[].
This is intentional - suggestions are lightweight for autocomplete.
// ProductSuggestion has:
{
(id, name, slug, image, basePrice, salePrice, type);
}
// Product has many more fields7. All Prices Are Strings - Use parseFloat()
// ❌ WRONG - assuming number
const total = item.price * quantity;
// ✅ CORRECT - parse first
const total = parseFloat(item.price) * quantity;
// Or use SDK helper
import { formatPrice } from 'brainerce';
const display = formatPrice(item.price, { currency: 'USD' });8. Variant Attributes Are Record<string, string> (Locale-Aware)
// Accessing variant attributes:
const color = variant.attributes?.['Color']; // string
const size = variant.attributes?.['Size']; // string
// Use getVariantOptions() for display — returns translated names when locale is active:
const options = getVariantOptions(variant);
// With Accept-Language: en → [{ name: "Color", value: "Red" }, { name: "Size", value: "M" }]
// With Accept-Language: he → [{ name: "צבע", value: "אדום" }, { name: "מידה", value: "M" }]i18n note: When
client.setLocale()is active, the backend translates both attribute keys (e.g. "צבע" → "Color") and option values (e.g. "אדום" → "Red") insidevariant.attributes. ThegetVariantOptions()andgetProductSwatches()helpers automatically return translated names.
9. Address Uses region, NOT state
// ❌ 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',
};10. OAuth - Use authorizationUrl, NOT url
// ❌ WRONG
const response = await client.getOAuthAuthorizeUrl('GOOGLE', { redirectUrl });
window.location.href = response.url; // 'url' doesn't exist!
// ✅ CORRECT
const response = await client.getOAuthAuthorizeUrl('GOOGLE', { redirectUrl });
window.location.href = response.authorizationUrl; // Correct property name11. OAuth Provider Type is Exported
// ❌ WRONG - creating your own type
type Provider = 'google' | 'facebook'; // lowercase won't work!
// ✅ CORRECT - import from SDK
import { CustomerOAuthProvider } from 'brainerce';
// CustomerOAuthProvider = 'GOOGLE' | 'FACEBOOK' | 'GITHUB' (UPPERCASE)
const provider: CustomerOAuthProvider = 'GOOGLE';
await client.getOAuthAuthorizeUrl(provider, { redirectUrl });12. getAvailableOAuthProviders Returns Object, Not Array
// ❌ WRONG - expecting array directly
const providers = await client.getAvailableOAuthProviders();
providers.forEach(p => ...); // Error! providers is not an array
// ✅ CORRECT - access the providers property
const response = await client.getAvailableOAuthProviders();
response.providers.forEach(p => ...); // response.providers is the array13. SDK Uses null, Not undefined
Optional fields in SDK types use null, not undefined:
// SDK types use:
slug: string | null;
salePrice: string | null;
// So when checking:
if (product.slug !== null) {
// ✅ Check for null
// ...
}14. Cart Has No total Field - Use getCartTotals() Helper
// ❌ 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 'brainerce';
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, NOTdiscount - Cart has NO
totalfield - usegetCartTotals()or calculate - Checkout DOES have a
totalfield, but Cart does not getCartTotals()works with all carts — guests now use server-side session carts with full pricing fields.
15. SearchSuggestions - Products Have price, Not basePrice
// 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 => (
<div>{p.name} - {formatPrice(p.price, { currency })}</div>
));16. Forgetting to Clear Cart After Payment
This causes "ghost items" in the cart after successful payment!
// ❌ WRONG - Cart items remain after payment!
// In your success page:
export default function SuccessPage() {
return <div>Thank you for your order!</div>;
// 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
client.completeGuestCheckout(checkoutId);
}
}, []);
return <div>Thank you for your order!</div>;
}Why is this needed?
completeGuestCheckout()sends the order to the server AND clears the session 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
- After successful checkout, also call
client.onCheckoutComplete()to clear the session cart reference
Checkout: Guest vs Logged-In Customer
Both guests and logged-in customers now use the same smart* cart methods. The SDK handles server-side session carts for guests automatically.
| Customer Type | Cart Method | Checkout |
| ------------- | --------------------------------- | ------------------------------------------- |
| Guest | smartAddToCart() (session cart) | startGuestCheckout() → createCheckout() |
| Logged In | smartAddToCart() (server cart) | createCheckout() → completeCheckout() |
Cart Usage (Same for Both)
// Add to cart — works for both guests and logged-in users
await client.smartAddToCart({ productId: 'prod_123', quantity: 2 });
// Get cart
const cart = await client.smartGetCart();
// Get cart with recommendations, upgrades, and bundles in a single request
const cartWithExtras = await client.smartGetCart({
include: ['recommendations', 'upgrades', 'bundles'],
});
// cartWithExtras.recommendations, cartWithExtras.upgrades, cartWithExtras.bundles
// Update quantity
await client.smartUpdateCartItem('prod_123', 5);
// Remove item
await client.smartRemoveFromCart('prod_123');
// Get totals (works for all carts)
import { getCartTotals } from 'brainerce';
const totals = getCartTotals(cart); // { subtotal, discount, shipping, total }On Login — Merge Guest Cart
// After setting customer token
client.setCustomerToken(token);
await client.syncCartOnLogin(); // Merges session cart into customer cartGuest Checkout
// Guest checkout creates a checkout from the session cart
const result = await client.startGuestCheckout();
if (result.tracked) {
const checkout = await client.getCheckout(result.checkoutId);
await client.setShippingAddress(result.checkoutId, shippingAddress);
// ... continue with shipping rates, payment, etc.
}Logged-In Customer Checkout (orders linked to account)
// 1. Make sure customer token is set (after login)
client.setCustomerToken(authResponse.token);
// 2. Add items to cart (smart methods handle server cart automatically)
await client.smartAddToCart({
productId: products[0].id,
quantity: 1,
});
// 3. Get cart and create checkout
const cart = await client.smartGetCart();
const checkout = await client.createCheckout({ cartId: cart.id });
// 4. Set customer info (REQUIRED - email is needed for order!)
await client.setCheckoutCustomer(checkout.id, {
email: '[email protected]',
firstName: 'John',
lastName: 'Doe',
});
// 5. Set shipping address
await client.setShippingAddress(checkout.id, {
firstName: 'John',
lastName: 'Doe',
line1: '123 Main St',
city: 'New York',
postalCode: '10001',
country: 'US',
});
// 6. Get shipping rates and select one
const rates = await client.getShippingRates(checkout.id);
await client.selectShippingMethod(checkout.id, rates[0].id);
// 7. Complete checkout - order is linked to customer!
const { orderId } = await client.completeCheckout(checkout.id);
console.log('Order created:', orderId);
// Customer can now see this order in client.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.
Cart (Unified for All Users)
The SDK uses server-side carts for all users. Guests get automatic session carts; logged-in customers get server carts linked to their account.
- ✅ Cart persists across page refreshes (via session token in localStorage)
- ✅ Server-side pricing, discounts, and totals
- ✅ Automatic migration from guest → customer cart on login
- ✅ Same API for both guests and logged-in users
// Add to cart (guest or logged-in — same code!)
await client.smartAddToCart({ productId: 'prod_123', quantity: 2 });
// Get cart
const cart = await client.smartGetCart();
console.log('Items:', cart.items.length);
console.log('Total:', getCartTotals(cart).total);
// Update quantity
await client.smartUpdateCartItem('prod_123', 5);
// Remove item
await client.smartRemoveFromCart('prod_123');After Login — Sync Cart
client.setCustomerToken(token);
const mergedCart = await client.syncCartOnLogin();
// Guest session cart items are merged into the customer's server cartAfter Checkout — Clear Cart
client.onCheckoutComplete();
// Clears session cart reference so next visit starts freshAfter Logout — Preserve Guest Cart
client.clearCustomerToken();
client.onLogout();
// Session cart is preserved — guest can continue browsingComplete Store Setup
Step 1: Create the Brainerce Client
Create a file lib/brainerce.ts:
import { BrainerceClient } from 'brainerce';
export const client = new BrainerceClient({
connectionId: 'vc_YOUR_CONNECTION_ID', // Your Connection ID from Brainerce
});
// ----- Cart Helpers -----
export async function getCart() {
return client.smartGetCart();
}
export function getCartItemCount(): number {
return client.getSmartCartItemCount();
}
// ----- Customer Token Helpers -----
export function setCustomerToken(token: string | null): void {
if (token) {
localStorage.setItem('customerToken', token);
client.setCustomerToken(token);
} else {
localStorage.removeItem('customerToken');
client.clearCustomerToken();
}
}
export function restoreCustomerToken(): string | null {
const token = localStorage.getItem('customerToken');
if (token) client.setCustomerToken(token);
return token;
}
export function isLoggedIn(): boolean {
return !!localStorage.getItem('customerToken');
}Important: Cart & Checkout Data Structures
Nested Product/Variant Structure
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:
// WRONG - product name is NOT at top level
const name = item.name; // undefined!
const sku = item.sku; // undefined!Correct Access Pattern:
// 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;Field Mapping Reference
| 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 |
Price Fields Are Strings
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:
// Monetary fields that are strings:
// - CartItem: unitPrice, discountAmount
// - Cart: subtotal, discountAmount
// - CheckoutLineItem: unitPrice, discountAmount
// - Checkout: subtotal, discountAmount, shippingAmount, taxAmount, total
// - ShippingRate: price
import { formatPrice } from 'brainerce';
// Option 1: Using formatPrice helper (recommended)
const cart = await client.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)}`);
});Complete Cart Item Display Example
import type { CartItem } from 'brainerce';
import { formatPrice } from 'brainerce';
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 (
<div className="flex items-center gap-4">
<img src={productImage} alt={displayName} className="w-16 h-16 object-cover" />
<div className="flex-1">
<h3 className="font-medium">{displayName}</h3>
<p className="text-sm text-gray-500">SKU: {item.variant?.sku || productSku}</p>
</div>
<span className="text-gray-600">Qty: {item.quantity}</span>
<span className="font-medium">${lineTotal.toFixed(2)}</span>
</div>
);
}API Reference
Categories, Brands & Tags (Filtering)
Get Categories (Tree Structure)
Categories are returned as a nested tree. Each category can have children, which can also have children (unlimited depth). Use this to build category filter UI with dropdowns for subcategories.
const { categories } = await client.getCategories();
// categories is a tree:
// [
// { id: "cat_1", name: "Jewelry", parentId: null, children: [
// { id: "cat_2", name: "Necklaces", parentId: "cat_1", children: [] },
// { id: "cat_3", name: "Rings", parentId: "cat_1", children: [
// { id: "cat_4", name: "Gold Rings", parentId: "cat_3", children: [] }
// ]}
// ]},
// { id: "cat_5", name: "Clothing", parentId: null, children: [] }
// ]Type:
interface CategoryNode {
id: string;
name: string;
parentId?: string | null;
children: CategoryNode[]; // Recursive — can nest to any depth
}Filter Products by Category
Pass a category ID to getProducts(). The backend automatically includes all subcategories. So filtering by a parent category returns products from that category AND all its descendants.
// Filter by parent "Jewelry" — returns products from Jewelry, Necklaces, Rings, Gold Rings
const { data } = await client.getProducts({ categories: 'cat_1' });
// Filter by specific subcategory "Rings" — returns products from Rings + Gold Rings
const { data } = await client.getProducts({ categories: 'cat_3' });
// Multiple categories
const { data } = await client.getProducts({ categories: ['cat_1', 'cat_5'] });Building Category Filter UI (Nested Dropdowns)
Complete example for a products page with hierarchical category chips:
'use client';
import { useEffect, useState, useRef } from 'react';
import { client } from '@/lib/brainerce';
interface CategoryNode {
id: string;
name: string;
parentId?: string | null;
children: CategoryNode[];
}
// Check if a category or any descendant matches the selected ID
function isActiveInTree(node: CategoryNode, selectedId: string): boolean {
if (node.id === selectedId) return true;
return node.children.some((child) => isActiveInTree(child, selectedId));
}
// Category chip — simple button for leaf categories, dropdown for parents
function CategoryChip({ category, selectedId, onSelect }: {
category: CategoryNode;
selectedId: string;
onSelect: (id: string) => void;
}) {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const hasChildren = category.children.length > 0;
const isActive = isActiveInTree(category, selectedId);
// Close on outside click
useEffect(() => {
if (!open) return;
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [open]);
// Simple chip for categories without children
if (!hasChildren) {
return (
<button
onClick={() => onSelect(category.id)}
className={selectedId === category.id ? 'chip active' : 'chip'}
>
{category.name}
</button>
);
}
// Chip with dropdown for categories with children
return (
<div ref={ref} style={{ position: 'relative' }}>
<button onClick={() => setOpen(!open)} className={isActive ? 'chip active' : 'chip'}>
{category.name} ▾
</button>
{open && (
<div className="dropdown">
<button onClick={() => { onSelect(category.id); setOpen(false); }}>
All {category.name}
</button>
<SubcategoryList nodes={category.children} depth={0} selectedId={selectedId}
onSelect={(id) => { onSelect(id); setOpen(false); }} />
</div>
)}
</div>
);
}
// Recursive list — renders children at any depth with indentation
function SubcategoryList({ nodes, depth, selectedId, onSelect }: {
nodes: CategoryNode[]; depth: number; selectedId: string; onSelect: (id: string) => void;
}) {
return (
<>
{nodes.map((node) => (
<div key={node.id}>
<button
onClick={() => onSelect(node.id)}
style={{ paddingInlineStart: `${(depth + 1) * 16}px` }}
className={selectedId === node.id ? 'active' : ''}
>
{node.name}
</button>
{node.children.length > 0 && (
<SubcategoryList nodes={node.children} depth={depth + 1}
selectedId={selectedId} onSelect={onSelect} />
)}
</div>
))}
</>
);
}
// Usage in products page:
function ProductFilters() {
const [categories, setCategories] = useState<CategoryNode[]>([]);
const [selectedCategory, setSelectedCategory] = useState('');
useEffect(() => {
client.getCategories().then(({ categories }) => setCategories(categories));
}, []);
useEffect(() => {
// Fetch products filtered by selected category
client.getProducts({
categories: selectedCategory || undefined,
}).then(/* update products state */);
}, [selectedCategory]);
return (
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<button onClick={() => setSelectedCategory('')}
className={!selectedCategory ? 'chip active' : 'chip'}>
All
</button>
{categories.map((cat) => (
<CategoryChip key={cat.id} category={cat}
selectedId={selectedCategory} onSelect={setSelectedCategory} />
))}
</div>
);
}Key points for AI builders:
getCategories()returns a tree — don't flatten it! Usechildrento build nested UI.- Selecting a parent category automatically includes all descendants (backend handles this).
- Use
position: relativeon the chip wrapper andposition: absoluteon the dropdown for proper overlay positioning. - Use
paddingInlineStart(notpaddingLeft) for RTL support. - Close dropdown on outside click to prevent multiple dropdowns staying open.
Get Brands & Tags
const { brands } = await client.getBrands();
// [{ id: "brand_1", name: "Nike" }, ...]
const { tags } = await client.getTags();
// [{ id: "tag_1", name: "Sale" }, ...]
// Use in product filtering:
const { data } = await client.getProducts({
brands: ['brand_1'],
tags: ['tag_1'],
categories: ['cat_1'], // Can combine all filters
});Products
Get Products (with pagination)
import { client } from '@/lib/brainerce';
import type { Product, PaginatedResponse } from 'brainerce';
const response: PaginatedResponse<Product> = await client.getProducts({
page: 1,
limit: 12,
search: 'shirt', // Optional: search by name
categories: 'cat_id', // Optional: filter by category (includes subcategories)
brands: ['brand_id'], // Optional: filter by brands
tags: ['tag_id'], // Optional: filter by tags
minPrice: 10, // Optional: minimum price
maxPrice: 100, // Optional: maximum price
metafields: { color: ['red', 'blue'] }, // Optional: filter by custom-field values
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 pagesGet Single Product
const product: Product = await client.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:
import type { SearchSuggestions } from 'brainerce';
// Basic autocomplete
const suggestions: SearchSuggestions = await client.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 client.getSearchSuggestions('dress', 3);Search covers: name, sku, description, categories, tags, and brands.
Example: Search Input with Suggestions
function SearchInput() {
const [query, setQuery] = useState('');
const [suggestions, setSuggestions] = useState<SearchSuggestions | null>(null);
// Debounce search requests
useEffect(() => {
if (query.length < 2) {
setSuggestions(null);
return;
}
const timer = setTimeout(async () => {
const results = await client.getSearchSuggestions(query, 5);
setSuggestions(results);
}, 300);
return () => clearTimeout(timer);
}, [query]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
/>
{suggestions && (
<div className="suggestions">
{suggestions.products.map((product) => (
<a key={product.id} href={`/products/${product.slug}`}>
<img src={product.image || '/placeholder.png'} alt={product.name} />
<span>{product.name}</span>
<span>${product.basePrice}</span>
</a>
))}
{suggestions.categories.map((category) => (
<a key={category.id} href={`/category/${category.id}`}>
{category.name} ({category.productCount} products)
</a>
))}
</div>
)}
</div>
);
}Product Type Definition
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';
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<string, string>;
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".
Important: Each metafield has a type field. When rendering, you must check field.type and render accordingly — don't just display field.value as text for all types.
| Type | Rendering |
| ----------------------------------------------------------- | ------------------------------------------------------- |
| IMAGE | <img> thumbnail |
| GALLERY | Row of <img> thumbnails (value is JSON array of URLs) |
| URL | <a> clickable link |
| COLOR | Color swatch circle + hex value |
| BOOLEAN | "Yes" / "No" |
| DATE | Formatted date (toLocaleDateString()) |
| DATETIME | Formatted datetime (toLocaleString()) |
| TEXT, TEXTAREA, NUMBER, DIMENSION, WEIGHT, JSON | Plain text |
import {
getProductMetafield,
getProductMetafieldValue,
getProductMetafieldsByType,
} from 'brainerce';
import type { ProductMetafield } from 'brainerce';
const product = await client.getProductBySlug('blue-shirt');
// Render metafield value based on type
function renderMetafieldValue(field: ProductMetafield): string | JSX.Element {
switch (field.type) {
case 'IMAGE':
return field.value ? <img src={field.value} alt={field.definitionName} className="h-16 w-16 rounded object-cover" /> : '-';
case 'GALLERY': {
let urls: string[] = [];
try { urls = JSON.parse(field.value); } catch { urls = field.value ? [field.value] : []; }
return <div className="flex gap-2">{urls.map((url, i) => <img key={i} src={url} className="h-16 w-16 rounded object-cover" />)}</div>;
}
case 'URL':
return field.value ? <a href={field.value} target="_blank" rel="noopener noreferrer">{field.value}</a> : '-';
case 'COLOR':
return <span><span style={{ backgroundColor: field.value }} className="inline-block h-4 w-4 rounded-full" /> {field.value}</span>;
case 'BOOLEAN':
return field.value === 'true' ? 'Yes' : 'No';
case 'DATE':
return field.value ? new Date(field.value).toLocaleDateString() : '-';
case 'DATETIME':
return field.value ? new Date(field.value).toLocaleString() : '-';
default:
return field.value || '-';
}
}
// Display in a spec table
product.metafields?.forEach((field) => {
console.log(`${field.definitionName}: ${renderMetafieldValue(field)}`);
});
// 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 client.getPublicMetafieldDefinitions();
definitions.forEach((def) => {
console.log(`${def.name} (${def.key}): ${def.type}, required: ${def.required}`);
});Note:
metafieldsmay be empty if the store hasn't defined custom fields. Always use optional chaining (product.metafields?.forEach).
Product Customization Fields (Customer Input)
Some products allow customers to provide custom input (e.g., "What text to write on the cake?", "Upload your logo"). These are returned in the customizationFields array on the product response.
import { getProductCustomizationFields } from 'brainerce';
import type { ProductCustomizationField } from 'brainerce';
const product = await client.getProductBySlug('custom-cake');
// Get customization fields for this product
const fields = getProductCustomizationFields(product);
fields.forEach((field) => {
// field.key: unique identifier (e.g., "cake_text")
// field.name: display label (e.g., "Text on Cake")
// field.type: TEXT, TEXTAREA, NUMBER, BOOLEAN, DATE, COLOR, IMAGE, GALLERY, etc.
// field.required: whether customer must fill this in
// field.minLength, field.maxLength: validation for text fields
// field.minValue, field.maxValue: validation for number fields
// field.enumValues: allowed options for dropdown fields
// field.defaultValue: default value to pre-fill
});
// Customer fills in values, then add to cart with metadata
await client.addToCart(cartId, {
productId: product.id,
quantity: 1,
metadata: {
cake_text: 'Happy Birthday!', // TEXT field
font_color: '#FF0000', // COLOR field
},
});For IMAGE/GALLERY fields, upload the file first:
// Upload customer file (storefront/vibe-coded mode)
const { url } = await client.uploadCustomizationFile(fileInput.files[0]);
// Then include the URL in cart metadata
await client.addToCart(cartId, {
productId: product.id,
quantity: 1,
metadata: {
logo: url, // IMAGE field value is the uploaded URL
},
});Supported field types:
| Type | Stored in metadata as | Customer UI |
| ---------------- | --------------------- | ------------------- |
| TEXT | string | Text input |
| TEXTAREA | string | Textarea |
| NUMBER | number | Number input |
| BOOLEAN | boolean | Checkbox |
| DATE/DATETIME | string (ISO) | Date picker |
| COLOR | string (hex) | Color picker |
| URL | string | URL input |
| IMAGE | string (URL) | File upload |
| GALLERY | string[] (URLs) | Multi-file upload |
| DIMENSION/WEIGHT | { value, unit } | Value + unit inputs |
Admin methods (require API key):
// Set which customer-input definitions apply to a product
await client.setProductCustomizationFields(productId, [definitionId1, definitionId2]);
// Get current assignments
const fields = await client.getProductCustomizationFields(productId);Note:
customizationFieldsis only present when the product has customer input fields assigned. After checkout, customization values are preserved in the order asitem.customizations.
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:
// 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 <span>${priceRange.min} - ${priceRange.max}</span>;
}
// Simple product or all variants same price - show single price
return product.salePrice ? (
<>
<span className="text-red-600">${product.salePrice}</span>
<span className="line-through text-gray-400 ml-2">${product.basePrice}</span>
</>
) : (
<span>${product.basePrice}</span>
);
}When to show price range:
- Product
typeis'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
typeis'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
<p>,<ul>,<li>tags instead of formatted content!
Use the SDK helper functions to handle this automatically:
import { isHtmlDescription, getDescriptionContent } from 'brainerce';
// 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 <div dangerouslySetInnerHTML={{ __html: product.description }} />;
}
// Plain text - render normally
return <p>{product.description}</p>;
}
// Option 2: Using getDescriptionContent helper
function ProductDescription({ product }: { product: Product }) {
const content = getDescriptionContent(product);
if (!content) return null;
if ('html' in content) {
return <div dangerouslySetInnerHTML={{ __html: content.html }} />;
}
return <p>{content.text}</p>;
}| 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:
// WRONG - HTML will show as raw tags like <p>Hello</p>
<p>{product.description}</p>Cart Operations (All Users)
The smart* methods work for both guests and logged-in users. Guests use server-side session carts; logged-in users use server carts linked to their account.
Add to Cart
await client.smartAddToCart({
productId: 'prod_123',
variantId: 'var_456', // Optional: for products with variants
quantity: 2,
});Get Cart
const cart = await client.smartGetCart();
console.log(cart.items); // Array of CartItem
console.log(cart.itemCount); // Total item count
console.log(cart.couponCode); // Applied coupon (if any)Update Item Quantity
// Set quantity to 5
await client.smartUpdateCartItem('prod_123', 5);
// For variant products
await client.smartUpdateCartItem('prod_123', 3, 'var_456');
// Set to 0 to remove
await client.smartUpdateCartItem('prod_123', 0);Remove Item
await client.smartRemoveFromCart('prod_123');
await client.smartRemoveFromCart('prod_123', 'var_456'); // With variantGet Cart Item Count (No API Call)
// Returns cached count from session reference — instant, no API call
const count = client.getSmartCartItemCount();
console.log(`${count} items in cart`);
// For accurate count, use: (await client.smartGetCart()).itemCountApply Coupon
const cart = await client.smartGetCart();
const updated = await client.applyCoupon(cart.id, 'SAVE20');
console.log(updated.discountAmount); // "10.00"
console.log(updated.couponCode); // "SAVE20"
// Remove coupon
await client.removeCoupon(cart.id);Cart Totals
import { getCartTotals } from 'brainerce';
const cart = await client.smartGetCart();
const totals = getCartTotals(cart);
// { subtotal: 59.98, discount: 10, shipping: 5.99, total: 55.97 }Guest Checkout (Submit Order)
Note:
startGuestCheckout()is the preferred method for guest checkout — it creates a full checkout session from the session cart.submitGuestOrder()still works as a simpler alternative for basic orders.
// Make sure cart has items, customer email, and shipping address
const order = await client.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 (Brainerce 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
// If you want to keep the cart data (e.g., for order review page)
const order = await client.submitGuestOrder({ clearCartOnSuccess: false });Create Order with Custom Data
If you manage cart state yourself instead of using the smart cart methods:
const order = await client.createGuestOrder({
items: [
{ productId: 'prod_123', quantity: 2 },
{ productId: 'prod_456', variantId: 'var_789', quantity: 1 },
],
customer: {
email: '[email protected]',
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
interface GuestOrderResponse {
orderId: string;
orderNumber: string;
status: string;
total: number;
message: string;
}Tracked Guest Checkout (Automatic)
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
- Go to Brainerce Admin → Integrations → Vibe-Coded Sites
- Click on your connection → Settings
- Enable "Track Guest Checkouts"
- 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):
// 1. Start tracked checkout (sends cart items to server)
const checkout = await client.startGuestCheckout();
if (checkout.tracked) {
// 2. Update with shipping address
await client.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 client.completeGuestCheckout(checkout.checkoutId);
console.log('Order created:', order.orderId);
} else {
// Fallback to regular guest checkout
const order = await client.submitGuestOrder();
}Response Types
type GuestCheckoutStartResponse =
| {
tracked: true;
checkoutId: string;
cartId: string;
message: string;
}
| {
tracked: false;
message: string;
};Server Cart (Low-Level API)
These low-level methods are available for advanced use cases. For most storefronts, use the smart* methods above instead.
Create Cart
const cart = await client.createCart();Get Cart
const cartId = getCartId();
if (cartId) {
const cart = await client.getCart(cartId);
console.log(cart.items); // CartItem[]
console.log(cart.itemCount); // Total items
console.log(cart.subtotal); // Subtotal amount
}Add to Cart
const cart = await client.addToCart(cartId, {
productId: 'product_id',
variantId: 'variant_id', // Optional: for VARIABLE products
quantity: 2,
notes: 'Gift wrap please', // Optional
});Update Cart Item
const cart = await client.updateCartItem(cartId, itemId, {
quantity: 3,
});Remove Cart Item
const cart = await client.removeCartItem(cartId, itemId);Apply Coupon
const cart = await client.applyCoupon(cartId, 'SAVE20');
console.log(cart.discountAmount); // Discount applied
console.log(cart.couponCode); // 'SAVE20'Remove Coupon
const cart = await client.removeCoupon(cartId);Cart Type Definition
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;
}Checkout
Create Checkout from Cart
const checkout = await client.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.
// 1. In your cart page, track selected items
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
// 2. Create checkout with only selected items
const checkout = await client.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 client.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 laterSet Customer Information
const checkout = await client.setCheckoutCustomer(checkoutId, {
email: '[email protected]',
firstName: 'John',
lastName: 'Doe',
phone: '+1234567890', // Optional
});Set Shipping Address
const { checkout, rates } = await client.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
const checkout = await client.selectShippingMethod(checkoutId, rates[0].id);Set Billing Address
// Same as shipping
const checkout = await client.setBillingAddress(checkoutId, {
...shippingAddress,
sameAsShipping: true, // Optional shortcut
});Complete Checkout
const { orderId } = await client.completeCheckout(checkoutId);
clearCartId(); // Clear cart from localStorage
console.log('Order created:', orderId);Checkout Custom Fields (Surcharges)
Some stores define custom fields at checkout (e.g., floor number, gift wrapping, installation) that can add surcharges to the order total.
import type { CheckoutCustomFieldDefinition } from 'brainerce';
// 1. Get applicable custom fields for this checkout
const fields = await client.getCheckoutCustomFields(checkoutId);
// 2. Render fields dynamically based on field.type:
// TEXT → text input, NUMBER → number input, BOOLEAN → checkbox,
// SELECT → dropdown (field.options), DATE → date picker
// Show pricing info from field.pricing (e.g., "+25₪ when checked")
// 3. Set customer values → recalculates surcharges and total
const updatedCheckout = await client.setCheckoutCustomFields(checkoutId, {
gift_wrapping: 'premium', // SELECT → surcharge based on selected option
floor_number: 5, // NUMBER → conditional surcharge (e.g., floor > 2 = +30₪)
installation: true, // BOOLEAN → surcharge when checked
});
// updatedCheckout.surchargeAmount = "55" (sum of all surcharges)
// updatedCheckout.appliedSurcharges = [{ key, name, value, amount }]
// updatedCheckout.total is recalculated with surcharges includedPricing types:
none→ no surchargefixed→ flat amount when field has a valueboolean_checked→ amount when checkbox is checkedper_option→ each SELECT option has its own priceconditional→ surcharge when NUMBER value meets condition (gt, gte, lt, lte, eq)
Checkout Type Definition
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:
// Step 1: Set shipping address - this returns available rates
const { checkout, rates } = await client.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 (
<div className="bg-yellow-50 p-4 rounded">
<p className="font-medium">No shipping options available</p>
<p className="text-sm text-gray-600">
We currently cannot ship to this address. Please try a different address or contact us for
assistance.
</p>
</div>
);
}
// Step 3: Display available rates to customer
<div className="space-y-2">
<h3 className="font-medium">Select Shipping Method</h3>
{rates.map((rate) => (
<label key={rate.id} className="flex items-center gap-3 p-3 border rounded cursor-pointer">
<input
type="radio"
name="shipping"
value={rate.id}
checked={selectedRateId === rate.id}
onChange={() => setSelectedRateId(rate.id)}
/>
<div className="flex-1">
<span className="font-medium">{rate.name}</span>
{rate.description && <p className="text-sm text-gray-500">{rate.description}</p>}
{rate.estimatedDays && (
<p className="text-sm text-gray-500">Estimated delivery: {rate.estimatedDays} business days</p>
)}
</div>
<span className="font-medium">${parseFloat(rate.price).toFixed(2)}</span>
</label>
))}
</div>;
// Step 4: Select the shipping method
await client.selectShippingMethod(checkoutId, selectedRateId);Handling Empty Shipping Rates:
When no shipping rates are available, you have several options:
// Option 1: Show helpful message
if (rates.length === 0) {
return <NoShippingAvailable address={shippingAddress} />;
}
// Option 2: Allow customer to contact store
if (rates.length === 0) {
return (
<div>
<p>Shipping not available to your location.</p>
<a href="/contact">Request a shipping quote</a>
</div>
);
}
// 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;
}Payment Integration (Vibe-Coded Sites)
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:
// For GUEST users (session cart):
const result = await client.startGuestCheckout();
const checkoutId = result.checkoutId;
// For LOGGED-IN users (server cart):
const cart = await client.smartGetCart();
const checkout = await client.createCheckout({ cartId: cart.id });
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:
const { hasPayments, providers, defaultProvider } = await client.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: ['p