> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shoppex.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Storefront SDK reference

> Complete method and type reference for the Shoppex Storefront SDK

This page lists every method and type in `@shoppexio/storefront`, grouped by object: Store, Products, Cart, Checkout, Reviews and invoices, and Types.

<Note>
  All async SDK methods return `SDKResponse<T>`, a wrapper containing `data`, `error`, and `status` fields. See [Response types](#response-types) for the full definition.
</Note>

## Store

The Store object gives access to your shop's public information, including name, branding, and settings.

<Tip>
  For simple cases such as displaying the store logo, use `getStoreLogoUrl()` or `getStoreBannerUrl()`. They are lightweight wrappers that avoid fetching the full store metadata.
</Tip>

### getStore

Fetches the store's public metadata.

```javascript theme={"system"}
const { data: store } = await shoppex.getStore();

console.log(store.name);     // "My Awesome Store"
console.log(store.currency); // "USD"
```

**Response**

<ResponseField name="data" type="Shop">
  <Expandable title="Shop object">
    <ResponseField name="id" type="string">
      Unique store identifier
    </ResponseField>

    <ResponseField name="name" type="string">
      Store display name
    </ResponseField>

    <ResponseField name="slug" type="string">
      URL-friendly store identifier
    </ResponseField>

    <ResponseField name="domain" type="string">
      Custom domain if configured
    </ResponseField>

    <ResponseField name="description" type="string">
      Store description
    </ResponseField>

    <ResponseField name="currency" type="string">
      Default currency (ISO 4217)
    </ResponseField>

    <ResponseField name="logo" type="string">
      Logo image URL
    </ResponseField>

    <ResponseField name="banner" type="string">
      Banner image URL
    </ResponseField>

    <ResponseField name="rating" type="number">
      Average store rating (1-5)
    </ResponseField>

    <ResponseField name="tos_enabled" type="boolean">
      Whether terms of service are enabled
    </ResponseField>

    <ResponseField name="social" type="object">
      Social media links

      <Expandable>
        <ResponseField name="discord" type="string">Discord URL</ResponseField>
        <ResponseField name="twitter" type="string">Twitter URL</ResponseField>
        <ResponseField name="instagram" type="string">Instagram URL</ResponseField>
        <ResponseField name="facebook" type="string">Facebook URL</ResponseField>
        <ResponseField name="telegram" type="string">Telegram URL</ResponseField>
        <ResponseField name="youtube" type="string">YouTube URL</ResponseField>
        <ResponseField name="reddit" type="string">Reddit URL</ResponseField>
        <ResponseField name="tiktok" type="string">TikTok URL</ResponseField>
      </Expandable>
    </ResponseField>
  </Expandable>
</ResponseField>

**Example: display store header**

```javascript theme={"system"}
async function renderStoreHeader() {
  const { data: store } = await shoppex.getStore();

  document.getElementById('store-header').innerHTML = `
    <img src="${store.logo}" alt="${store.name}" class="logo" style="border-radius: 9999px;">
    <h1>${store.name}</h1>
    ${store.rating ? `<span class="rating">${store.rating.toFixed(1)} / 5</span>` : ''}
  `;
}
```

### getStoreLogoUrl

Returns the store's logo URL directly.

```javascript theme={"system"}
const logoUrl = await shoppex.getStoreLogoUrl();

if (logoUrl) {
  document.getElementById('logo').src = logoUrl;
}
```

**Response**

<ResponseField name="return" type="string | null">
  Logo URL or `null` if no logo is set
</ResponseField>

### getStoreBannerUrl

Returns the store's banner URL directly.

```javascript theme={"system"}
const bannerUrl = await shoppex.getStoreBannerUrl();

if (bannerUrl) {
  document.getElementById('hero').style.backgroundImage = `url(${bannerUrl})`;
}
```

**Response**

<ResponseField name="return" type="string | null">
  Banner URL or `null` if no banner is set
</ResponseField>

### Store error handling

```javascript theme={"system"}
try {
  const { data, success, message } = await shoppex.getStore();

  if (!success) {
    console.error('Failed to load store:', message);
    return;
  }

  // Use store data
} catch (error) {
  if (error.name === 'NotInitializedError') {
    console.error('SDK not initialized. Call shoppex.init() first.');
  } else if (error.name === 'NetworkError') {
    console.error('Network error. Check your connection.');
  }
}
```

## Products

Fetch products from your store, including variants, addons, and custom fields.

### getProducts

Fetches all products from the store.

```javascript theme={"system"}
const { data: products } = await shoppex.getProducts();

products.forEach(product => {
  console.log(product.title, product.price);
});
```

**Response**

<ResponseField name="data" type="Product[]">
  Array of products
</ResponseField>

<Expandable title="Product object">
  <ResponseField name="uniqid" type="string">
    Unique product identifier
  </ResponseField>

  <ResponseField name="title" type="string">
    Product name
  </ResponseField>

  <ResponseField name="price" type="string">
    Base price as string (for precision). Use `shoppex.formatPrice()` to display.
  </ResponseField>

  <ResponseField name="currency" type="string">
    Currency code (ISO 4217)
  </ResponseField>

  <ResponseField name="slug" type="string">
    URL-friendly product identifier
  </ResponseField>

  <ResponseField name="description" type="string">
    Full product description (HTML)
  </ResponseField>

  <ResponseField name="description_tabs" type="{ title: string; content: string }[]">
    Merchant-defined extra description tabs (for example, Features, Specifications). `content` is HTML. Empty array when the product has no extra tabs.
  </ResponseField>

  <ResponseField name="images" type="ProductImage[]">
    Array of product images
  </ResponseField>

  <ResponseField name="cdn_image_url" type="string | null">
    Optimized cover image for cards, listings, search results, and other non-zoomed UI.
  </ResponseField>

  <ResponseField name="detail_image_url" type="string | null">
    High-resolution primary image for product detail pages, galleries, and zoom views.
  </ResponseField>

  <ResponseField name="video_link" type="string | null">
    Optional product video URL (YouTube, Streamable, or Vimeo). `null` when no video is configured.
  </ResponseField>

  <ResponseField name="variants" type="ProductVariant[]">
    Available variants (for example, size, color)
  </ResponseField>

  <ResponseField name="addons" type="ProductAddon[]">
    Optional add-ons
  </ResponseField>

  <ResponseField name="price_variants" type="PriceVariant[]">
    Price-based variants (for example, subscription tiers)
  </ResponseField>

  <ResponseField name="custom_fields" type="CustomFieldDefinition[]">
    Custom input fields for checkout
  </ResponseField>

  <ResponseField name="stock" type="number">
    Available stock quantity
  </ResponseField>

  <ResponseField name="categories" type="ProductCategory[]">
    Product categories as objects with `uniqid` and `title`
  </ResponseField>
</Expandable>

<Note>
  Product prices are returned as `string` types to preserve decimal precision. Always use `shoppex.formatPrice()` for display. Do not calculate directly with price strings.
</Note>

<Note>
  Use product images by surface:

  * `cdn_image_url` for product cards, category grids, cart rows, and search results
  * `detail_image_url` for product detail pages, image galleries, and zoom
  * `images[]` for the full gallery

  Simple example: if your custom storefront currently renders the PDP hero from `cdn_image_url`, switch that hero to `detail_image_url` to get the higher-resolution image.
</Note>

**Example: product grid**

```javascript theme={"system"}
async function renderProducts() {
  const { data: products } = await shoppex.getProducts();
  const container = document.getElementById('products');

  container.innerHTML = products.map(product => `
    <div class="product-card">
      <img src="${product.cdn_image_url || product.images[0]?.url || ''}" alt="${product.title}" style="border-radius: 8px;">
      <h3>${product.title}</h3>
      <span class="price">
        ${shoppex.formatPrice(product.price, product.currency)}
      </span>
      ${product.stock < 10 ? '<span class="low-stock">Only ' + product.stock + ' left!</span>' : ''}
    </div>
  `).join('');
}
```

### getProduct

Fetches a single product by ID or slug.

```javascript theme={"system"}
const { data: product } = await shoppex.getProduct('prod_abc123');

console.log(product.title);
console.log(product.variants);
```

**Parameters**

<ParamField path="idOrSlug" type="string" required>
  Product unique ID or URL slug
</ParamField>

**Example: product detail page**

```javascript theme={"system"}
async function renderProductDetail(productId) {
  const { data: product } = await shoppex.getProduct(productId);
  const galleryImages = product.images?.length
    ? product.images
    : [{ url: product.detail_image_url || product.cdn_image_url, alt: product.title }].filter(img => img.url);

  document.getElementById('product-detail').innerHTML = `
    <div class="gallery">
      ${galleryImages.map(img => `<img src="${img.url}" alt="${img.alt || product.title}" style="border-radius: 8px;">`).join('')}
    </div>
    <div class="info">
      <h1>${product.title}</h1>
      <p class="price">${shoppex.formatPrice(product.price, product.currency)}</p>
      <div class="description">${product.description}</div>

      ${product.variants?.length ? `
        <select id="variant-select">
          ${product.variants.map(v => `
            <option value="${v.id}">${v.title}</option>
          `).join('')}
        </select>
      ` : ''}

      ${product.video_link ? `
        <div class="product-video">
          <iframe src="${product.video_link}" title="${product.title}" allowfullscreen></iframe>
        </div>
      ` : ''}

      <button onclick="addToCart('${product.uniqid}')">Add to Cart</button>
    </div>
  `;
}
```

### getCategories

Fetches all unique product category IDs from your store.

```javascript theme={"system"}
const { data: categories } = await shoppex.getCategories();

// Returns array of category uniqids (strings)
// ["cat_abc123", "cat_def456", "cat_ghi789"]
```

**Example: category filter**

```javascript theme={"system"}
async function renderCategoryFilter() {
  const { data: categories } = await shoppex.getCategories();

  document.getElementById('category-filter').innerHTML = `
    <select onchange="filterByCategory(this.value)">
      <option value="">All Categories</option>
      ${categories.map(cat => `
        <option value="${cat}">${cat}</option>
      `).join('')}
    </select>
  `;
}

async function filterByCategory(category) {
  const { data: products } = await shoppex.getProducts();

  const filtered = category
    ? products.filter(p => p.categories?.includes(category))
    : products;

  renderProducts(filtered);
}
```

### Product groups

Stores can organize products into groups (for example, "Server Boosts", "Tokens"). Groups come from `getStorefront()`.

```javascript theme={"system"}
const { data: storefront } = await shoppex.getStorefront();

storefront.groups.forEach(group => {
  console.log(group.title, group.products_count);
});
```

<Warning>
  Breaking change in `@shoppexio/storefront` 1.0.0 (and the underlying storefront API): groups no longer embed full product objects in `products_bound`. Each group now carries `product_uniqids`, an array of product references, and every public product, standalone and group-bound, appears exactly once in the flat products list. If your integration reads `group.products_bound`, it sees `undefined` and must migrate to the lookup pattern below.
</Warning>

**Group object**

<ResponseField name="uniqid" type="string">
  Unique group identifier
</ResponseField>

<ResponseField name="title" type="string">
  Group name
</ResponseField>

<ResponseField name="product_uniqids" type="string[]">
  References to the group's products. Resolve them against the flat products list. The full product objects are not embedded in the group.
</ResponseField>

<ResponseField name="products_count" type="number">
  Number of products in the group
</ResponseField>

<ResponseField name="sort_priority" type="number">
  Display order of the group
</ResponseField>

**Resolving group products**

Build a lookup from the flat products list and resolve each group's references.

```javascript theme={"system"}
const { data: storefront } = await shoppex.getStorefront();

const byUniqid = new Map(storefront.products.map(p => [p.uniqid, p]));

storefront.groups.forEach(group => {
  const groupProducts = (group.product_uniqids ?? [])
    .map(uniqid => byUniqid.get(uniqid))
    .filter(Boolean);

  console.log(group.title, groupProducts.map(p => p.title));
});
```

If you install the SDK from npm, the same resolution is available as a named import.

```javascript theme={"system"}
import { getStorefrontGroupProducts } from '@shoppexio/storefront';

const groupProducts = getStorefrontGroupProducts(group, storefront.products);
```

<Note>
  Migrating from 0.3.x? Replace every `group.products_bound` read with the lookup above. `getProducts()` now returns the complete flat catalog, group-bound products included, so you no longer need to merge group products into your listing yourself. If you call the REST API directly, `/v1/storefront/products/public/:slug` groups carry `product_uniqids`. The `/v1/storefront/products/shop/:name` endpoint no longer returns groups at all. Read groups from the public catalog or bootstrap payload instead.
</Note>

### Working with variants

Products can have multiple variant types.

**Standard variants**

Variants such as size or color that do not change the price.

```javascript theme={"system"}
const product = await shoppex.getProduct('prod_abc');

product.variants.forEach(variant => {
  console.log(variant.id, variant.title);
  // "var_1", "Small"
  // "var_2", "Medium"
  // "var_3", "Large"
});
```

**Price variants**

Variants that have different prices.

```javascript theme={"system"}
product.price_variants.forEach(pv => {
  console.log(pv.id, pv.label, pv.price);
  // "pv_1", "Basic", 9.99
  // "pv_2", "Pro", 29.99
  // "pv_3", "Enterprise", 99.99
});
```

**Addons**

Optional extras the customer can add.

```javascript theme={"system"}
product.addons.forEach(addon => {
  console.log(addon.id, addon.name, addon.price, addon.required);
  // "addon_1", "Priority Support", 5.00, false
  // "addon_2", "Extended Warranty", 10.00, false
});
```

### Products error handling

```javascript theme={"system"}
const { data, success, message } = await shoppex.getProduct('invalid-id');

if (!success) {
  console.error('Product not found:', message);
  // Show 404 page or redirect
}
```

## Cart

The Cart object manages shopping cart state in the browser's localStorage. Cart data stays saved across page refreshes and browser sessions.

<Note>
  Cart data is stored in the browser's `localStorage`. If the user clears browser storage, the cart is lost.
</Note>

### getCart

Returns all items currently in the cart.

```javascript theme={"system"}
const cartItems = shoppex.getCart();

cartItems.forEach(item => {
  console.log(item.product_id, item.quantity);
});
```

**Response**

<ResponseField name="return" type="CartItem[]">
  <Expandable title="CartItem object">
    <ResponseField name="product_id" type="string">
      Product unique identifier
    </ResponseField>

    <ResponseField name="variant_id" type="string">
      Selected variant ID (empty string if no variant)
    </ResponseField>

    <ResponseField name="quantity" type="number">
      Item quantity
    </ResponseField>

    <ResponseField name="addons" type="CartAddon[]">
      Selected add-ons
    </ResponseField>

    <ResponseField name="custom_fields" type="Record<string, string>">
      Custom field values
    </ResponseField>

    <ResponseField name="price_variant_id" type="string">
      Selected price variant ID
    </ResponseField>
  </Expandable>
</ResponseField>

### getCartItemCount

Returns the total number of items in the cart.

```javascript theme={"system"}
const count = shoppex.getCartItemCount();
document.getElementById('cart-badge').textContent = count;
```

### addToCart

Adds an item to the cart, or increments quantity if it already exists.

```javascript theme={"system"}
// Basic usage
shoppex.addToCart('prod_abc123', 'var_001', 1);

// With options
shoppex.addToCart('prod_abc123', 'var_001', 2, {
  addons: [{ id: 'addon_1', quantity: 1 }],
  custom_fields: { 'License Name': 'John Doe' },
  price_variant_id: 'pv_pro'
});
```

**Parameters**

<ParamField path="productId" type="string" required>
  Product unique identifier
</ParamField>

<ParamField path="variantId" type="string" required>
  Variant ID. Use empty string `''` for products without variants.
</ParamField>

<ParamField path="quantity" type="number" default="1">
  Number of items to add
</ParamField>

<ParamField path="options" type="CartAddOptions">
  <Expandable title="CartAddOptions">
    <ParamField path="addons" type="CartAddon[]">
      Add-ons to include
    </ParamField>

    <ParamField path="custom_fields" type="Record<string, string>">
      Custom field values
    </ParamField>

    <ParamField path="price_variant_id" type="string">
      Selected price variant
    </ParamField>
  </Expandable>
</ParamField>

**Example: add to cart button**

```javascript theme={"system"}
function handleAddToCart(productId, variantId) {
  const quantity = parseInt(document.getElementById('quantity').value) || 1;

  // Collect selected addons
  const addons = [];
  document.querySelectorAll('.addon-checkbox:checked').forEach(cb => {
    addons.push({ id: cb.value, quantity: 1 });
  });

  shoppex.addToCart(productId, variantId, quantity, { addons });

  // Update UI
  updateCartBadge();
  showNotification('Added to cart!');
}
```

### updateCartItem

Updates an existing cart item.

```javascript theme={"system"}
shoppex.updateCartItem('prod_abc123', 'var_001', {
  quantity: 5,
  addons: [{ id: 'addon_2', quantity: 1 }]
});
```

**Parameters**

<ParamField path="productId" type="string" required>
  Product unique identifier
</ParamField>

<ParamField path="variantId" type="string" required>
  Variant ID
</ParamField>

<ParamField path="updates" type="object" required>
  <Expandable title="Update fields">
    <ParamField path="quantity" type="number">
      New quantity
    </ParamField>

    <ParamField path="addons" type="CartAddon[]">
      Updated add-ons
    </ParamField>

    <ParamField path="custom_fields" type="Record<string, string>">
      Updated custom fields
    </ParamField>
  </Expandable>
</ParamField>

### removeFromCart

Removes an item from the cart.

```javascript theme={"system"}
shoppex.removeFromCart('prod_abc123', 'var_001');
```

**Parameters**

<ParamField path="productId" type="string" required>
  Product unique identifier
</ParamField>

<ParamField path="variantId" type="string" required>
  Variant ID
</ParamField>

### clearCart

Removes all items from the cart.

```javascript theme={"system"}
shoppex.clearCart();
```

### Cart backup

The SDK can back up the cart before checkout, to restore it if checkout is cancelled.

**createCartBackup**

```javascript theme={"system"}
// Called automatically before checkout
shoppex.createCartBackup();
```

**restoreCartFromBackup**

```javascript theme={"system"}
// Restore cart after cancelled checkout
const restored = shoppex.restoreCartFromBackup();

if (restored) {
  console.log('Cart restored');
} else {
  console.log('No backup available');
}
```

### Complete cart UI example

```html theme={"system"}
<div id="cart">
  <h2>Shopping Cart</h2>
  <div id="cart-items"></div>
  <div id="cart-total"></div>
  <button onclick="goToCheckout()">Checkout</button>
</div>

<script>
async function renderCart() {
  const items = shoppex.getCart();
  const container = document.getElementById('cart-items');

  if (items.length === 0) {
    container.innerHTML = '<p>Your cart is empty</p>';
    return;
  }

  // Fetch product details for display
  const { data: products } = await shoppex.getProducts();
  const productMap = new Map(products.map(p => [p.uniqid, p]));

  let total = 0;

  container.innerHTML = items.map(item => {
    const product = productMap.get(item.product_id);
    if (!product) return '';

    const itemTotal = product.price * item.quantity;
    total += itemTotal;

    return `
      <div class="cart-item">
        <img src="${product.images[0]?.url}" alt="${product.title}" style="border-radius: 8px;">
        <div class="item-info">
          <h4>${product.title}</h4>
          <p>${shoppex.formatPrice(product.price, product.currency)}</p>
        </div>
        <div class="quantity">
          <button onclick="updateQuantity('${item.product_id}', '${item.variant_id}', ${item.quantity - 1})">-</button>
          <span>${item.quantity}</span>
          <button onclick="updateQuantity('${item.product_id}', '${item.variant_id}', ${item.quantity + 1})">+</button>
        </div>
        <button onclick="removeItem('${item.product_id}', '${item.variant_id}')" class="remove">Remove</button>
      </div>
    `;
  }).join('');

  document.getElementById('cart-total').innerHTML = `
    <strong>Total: ${shoppex.formatPrice(total, products[0]?.currency || 'USD')}</strong>
  `;
}

function updateQuantity(productId, variantId, newQuantity) {
  if (newQuantity < 1) {
    shoppex.removeFromCart(productId, variantId);
  } else {
    shoppex.updateCartItem(productId, variantId, { quantity: newQuantity });
  }
  renderCart();
}

function removeItem(productId, variantId) {
  shoppex.removeFromCart(productId, variantId);
  renderCart();
}

renderCart();
</script>
```

## Checkout

The Checkout object redirects customers to Shoppex hosted checkout. Checkout is fully hosted by Shoppex for PCI compliance.

### checkout

Redirects the customer to the checkout page with their cart contents.

```javascript theme={"system"}
await shoppex.checkout();
```

**Parameters**

<ParamField path="coupon" type="string">
  Pre-applied coupon code
</ParamField>

<ParamField path="options" type="CheckoutOptions">
  <Expandable title="CheckoutOptions">
    <ParamField path="autoRedirect" type="boolean" default="true">
      Automatically redirect to checkout page
    </ParamField>

    <ParamField path="locale" type="string">
      Override checkout language
    </ParamField>
  </Expandable>
</ParamField>

**Example: checkout with coupon**

```javascript theme={"system"}
const couponCode = document.getElementById('coupon-input').value;

if (couponCode) {
  // Validate coupon first
  const { data } = await shoppex.validateCoupon(couponCode);

  if (!data.valid) {
    alert('Invalid coupon code');
    return;
  }
}

// Proceed to checkout
await shoppex.checkout(couponCode);
```

### buildCheckoutUrl

Builds the checkout URL without redirecting. Use this to open checkout in a new tab or iframe.

```javascript theme={"system"}
const checkoutUrl = await shoppex.buildCheckoutUrl('SAVE10', { locale: 'de' });
console.log(checkoutUrl);
// https://yourstore.shoppex.io/checkout?coupon=SAVE10&locale=de&cart=...
```

**Parameters**

<ParamField path="coupon" type="string">
  Coupon code to pre-apply
</ParamField>

<ParamField path="locale" type="string">
  Checkout language (for example, 'en', 'de', 'fr')
</ParamField>

**Example: open checkout in new tab**

```javascript theme={"system"}
async function openCheckoutInNewTab() {
  const url = await shoppex.buildCheckoutUrl();
  window.open(url, '_blank');
}
```

### buildCheckoutUrlSync (deprecated)

<Note>
  `buildCheckoutUrlSync` is deprecated and throws immediately. Use `buildCheckoutUrl()` instead. It handles both default and custom domains.
</Note>

### Coupons

**validateCoupon**

Validates a coupon code before checkout. Affiliate and referral codes are separate from coupons. Use `validateAffiliateCode` or `applyAffiliateCode` for those instead.

```javascript theme={"system"}
const { data } = await shoppex.validateCoupon('SAVE10', {
  productId: 'prod_abc123',
  variantId: 'variant_lifetime',
});

if (data.valid) {
  console.log('Discount:', data.discount, data.discount_type);
  // 10, "percentage" → 10% off
  // 5.00, "fixed" → $5 off
} else {
  console.log('Invalid or expired coupon');
}
```

**Parameters**

<ParamField path="code" type="string" required>
  Coupon code to validate
</ParamField>

<ParamField path="options.productId" type="string">
  Product ID to check product-specific coupons
</ParamField>

<ParamField path="options.variantId" type="string">
  Selected variant ID to check variant-specific coupons. Requires `options.productId`.
</ParamField>

**Response**

<ResponseField name="data" type="CouponValidation">
  <Expandable title="CouponValidation">
    <ResponseField name="valid" type="boolean">
      Whether the coupon is valid
    </ResponseField>

    <ResponseField name="discount" type="number">
      Discount amount
    </ResponseField>

    <ResponseField name="discount_type" type="string">
      Either "percentage" or "fixed"
    </ResponseField>

    <ResponseField name="product_restricted" type="boolean">
      Whether the coupon is restricted to selected products
    </ResponseField>

    <ResponseField name="variant_restricted" type="boolean">
      Whether the coupon is restricted to selected variants
    </ResponseField>

    <ResponseField name="restriction_scope" type="string">
      One of "all", "products", "variants", or "products\_and\_variants"
    </ResponseField>

    <ResponseField name="allowed_product_ids" type="string[]">
      Public product IDs the coupon can apply to
    </ResponseField>

    <ResponseField name="allowed_variant_ids" type="string[]">
      Variant IDs the coupon can apply to
    </ResponseField>
  </Expandable>
</ResponseField>

### Affiliate codes

**validateAffiliateCode**

Validates an affiliate or referral code without storing it.

```javascript theme={"system"}
const result = await shoppex.validateAffiliateCode('creator10');

if (result.success && result.data.valid) {
  console.log(result.data.affiliate_code);
} else if (result.data?.program_enabled === false) {
  console.log('Affiliate program is disabled for this shop');
}
```

**applyAffiliateCode**

Validates and stores the normalized affiliate code. `checkout()` sends the stored code as `affiliate_code`.

```javascript theme={"system"}
const affiliate = await shoppex.applyAffiliateCode('creator10');

await shoppex.checkout({
  coupon: 'SAVE10',
  affiliateCode: affiliate.data?.affiliate_code,
});
```

**Example: coupon input with validation**

```html theme={"system"}
<div class="coupon-form">
  <input type="text" id="coupon-input" placeholder="Enter coupon code">
  <button onclick="applyCoupon()">Apply</button>
  <span id="coupon-status"></span>
</div>

<script>
let appliedCoupon = null;

async function applyCoupon() {
  const code = document.getElementById('coupon-input').value.trim();
  const status = document.getElementById('coupon-status');

  if (!code) {
    status.textContent = '';
    return;
  }

  const { data } = await shoppex.validateCoupon(code);

  if (data.valid) {
    appliedCoupon = code;
    if (data.discount_type === 'percentage') {
      status.textContent = `${data.discount}% off applied!`;
    } else {
      status.textContent = `$${data.discount} off applied!`;
    }
    status.className = 'success';
  } else {
    appliedCoupon = null;
    status.textContent = 'Invalid coupon';
    status.className = 'error';
  }
}

async function goToCheckout() {
  await shoppex.checkout(appliedCoupon);
}
</script>
```

### Checkout flow

```
┌─────────────────────────────────────────────────────────────────┐
│                        Your Website                             │
├─────────────────────────────────────────────────────────────────┤
│  1. Customer browses products                                   │
│  2. Adds items to cart (SDK stores in localStorage)            │
│  3. Clicks "Checkout"                                          │
│  4. SDK calls shoppex.checkout()                      │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                     Shoppex Checkout                            │
│                   (yourstore.shoppex.io)                        │
├─────────────────────────────────────────────────────────────────┤
│  5. Customer enters email, billing info                        │
│  6. Selects payment method (Stripe, PayPal, Crypto)           │
│  7. Completes payment                                          │
│  8. Receives confirmation + delivery                           │
└─────────────────────────────────────────────────────────────────┘
```

### After checkout

After successful checkout, the cart is automatically cleared. If you need to handle the return, read the order ID from the URL.

```javascript theme={"system"}
// Check URL for order confirmation
const urlParams = new URLSearchParams(window.location.search);
const orderId = urlParams.get('order');

if (orderId) {
  // Customer returned from successful checkout
  showOrderConfirmation(orderId);
}
```

## Reviews and invoices

This section documents two separate objects: shop reviews and invoice status.

### getShopReviews

Fetches all public reviews for the store.

```javascript theme={"system"}
const { data: reviews } = await shoppex.getShopReviews();

reviews.forEach(review => {
  console.log(review.rating, review.comment);
});
```

**Response**

<ResponseField name="data" type="Feedback[]">
  <Expandable title="Feedback object">
    <ResponseField name="id" type="string">
      Review unique identifier
    </ResponseField>

    <ResponseField name="rating" type="number">
      Star rating (1-5)
    </ResponseField>

    <ResponseField name="comment" type="string">
      Review text
    </ResponseField>

    <ResponseField name="created_at" type="string">
      ISO timestamp
    </ResponseField>
  </Expandable>
</ResponseField>

**Example: reviews section**

```javascript theme={"system"}
async function renderReviews() {
  const { data: reviews } = await shoppex.getShopReviews();
  const container = document.getElementById('reviews');

  // Calculate average
  const avgRating = reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length;

  container.innerHTML = `
    <div class="reviews-header">
      <h2>Customer Reviews</h2>
      <div class="average">
        ${'★'.repeat(Math.round(avgRating))}${'☆'.repeat(5 - Math.round(avgRating))}
        <span>${avgRating.toFixed(1)} / 5</span>
        <span>(${reviews.length} reviews)</span>
      </div>
    </div>

    <div class="reviews-list">
      ${reviews.map(review => `
        <div class="review">
          <div class="stars">${'★'.repeat(review.rating)}${'☆'.repeat(5 - review.rating)}</div>
          <p class="comment">${review.comment || ''}</p>
          <span class="date">${new Date(review.created_at).toLocaleDateString()}</span>
        </div>
      `).join('')}
    </div>
  `;
}
```

### getInvoice

Fetches full invoice details.

```javascript theme={"system"}
const { data: invoice } = await shoppex.getInvoice('inv_abc123');

console.log(invoice.status);   // "COMPLETED"
console.log(invoice.total);    // 29.99
console.log(invoice.products); // [{ title, quantity, price }]
```

**Response**

<ResponseField name="data" type="Invoice">
  <Expandable title="Invoice object">
    <ResponseField name="uniqid" type="string">
      Invoice unique identifier
    </ResponseField>

    <ResponseField name="status" type="string">
      Invoice status (PENDING, COMPLETED, CANCELLED, and more)
    </ResponseField>

    <ResponseField name="total" type="number">
      Total amount
    </ResponseField>

    <ResponseField name="currency" type="string">
      Currency code
    </ResponseField>

    <ResponseField name="products" type="InvoiceProduct[]">
      Purchased products
    </ResponseField>
  </Expandable>
</ResponseField>

### getInvoiceStatus

Lightweight endpoint for status polling. Use this instead of `getInvoice` for real-time updates.

```javascript theme={"system"}
const { data } = await shoppex.getInvoiceStatus('inv_abc123');
console.log(data.status); // "PENDING" | "COMPLETED" | "CANCELLED"
```

**Example: order status page**

```javascript theme={"system"}
async function pollOrderStatus(invoiceId) {
  const statusEl = document.getElementById('order-status');

  const checkStatus = async () => {
    const { data } = await shoppex.getInvoiceStatus(invoiceId);

    switch (data.status) {
      case 'PENDING':
        statusEl.innerHTML = '<span class="pending">Waiting for payment...</span>';
        break;
      case 'PROCESSING':
        statusEl.innerHTML = '<span class="processing">Processing payment...</span>';
        break;
      case 'COMPLETED':
        statusEl.innerHTML = '<span class="success">Order complete! Check your email.</span>';
        clearInterval(pollInterval);
        break;
      case 'CANCELLED':
        statusEl.innerHTML = '<span class="error">Order cancelled</span>';
        clearInterval(pollInterval);
        break;
    }
  };

  // Poll every 5 seconds
  const pollInterval = setInterval(checkStatus, 5000);
  checkStatus(); // Initial check
}

// Get invoice ID from URL
const invoiceId = new URLSearchParams(window.location.search).get('invoice');
if (invoiceId) {
  pollOrderStatus(invoiceId);
}
```

### Formatting utilities

**formatPrice**

Formats a price with a currency symbol.

```javascript theme={"system"}
shoppex.formatPrice(29.99, 'USD');       // "$29.99"
shoppex.formatPrice(29.99, 'EUR');       // "€29.99"
shoppex.formatPrice(29.99, 'EUR', 'de'); // "29,99 €"
```

**Parameters**

<ParamField path="amount" type="number" required>
  Price amount
</ParamField>

<ParamField path="currency" type="string" default="Store currency">
  ISO 4217 currency code
</ParamField>

<ParamField path="locale" type="string" default="en">
  Locale for formatting
</ParamField>

**createFormatter**

Creates a reusable `Intl.NumberFormat` instance.

```javascript theme={"system"}
const formatter = shoppex.createFormatter('EUR', 'de');

formatter.format(29.99);  // "29,99 €"
formatter.format(100);    // "100,00 €"
```

## Types

The SDK is written in TypeScript and exports all type definitions. Install with npm to get full IntelliSense support.

### Configuration types

```typescript theme={"system"}
interface ShoppexConfig {
  storeSlug: string;
  locale?: string;
  currency?: string;
  apiBaseUrl?: string;
}

interface ShoppexInitOptions {
  locale?: string;
  currency?: string;
  apiBaseUrl?: string;
}
```

### Response types

```typescript theme={"system"}
interface SDKResponse<T> {
  success: boolean;
  data?: T;
  message?: string;
}
```

### Store types

```typescript theme={"system"}
interface Shop {
  id: string;
  name: string;
  slug: string;
  domain?: string;
  description?: string;
  currency: string;
  logo?: string;
  banner?: string;
  rating?: number;
  tos_enabled?: boolean;
}
```

### Product types

```typescript theme={"system"}
interface ProductCategory {
  uniqid: string;
  title: string;
}

interface Product {
  uniqid: string;
  title: string;
  slug?: string;
  description?: string;
  price: string;           // String for precision
  price_display?: string;  // Formatted display price
  currency: string;
  stock?: number;
  cdn_image_url?: string | null;    // Card/listing/search image
  detail_image_url?: string | null; // PDP/gallery/zoom image
  images: ProductImage[];
  variants?: ProductVariant[];
  addons?: ProductAddon[];
  price_variants?: PriceVariant[];
  custom_fields?: string | unknown[] | null;
  categories?: ProductCategory[];
}

interface ProductImage {
  id: string;
  url: string;
  cloudflare_image_id?: string;
  alt?: string;
}

interface ProductVariant {
  id: string;
  title: string;
  price?: number;
  stock?: number;
}

interface ProductAddon {
  id: string;
  name: string;
  price: number;
  required?: boolean;
}

interface PriceVariant {
  id: string;
  label: string;
  price: number;
}

interface CustomFieldDefinition {
  id: string;
  name: string;
  type: 'text' | 'textarea' | 'select' | 'checkbox';
  required?: boolean;
  options?: string[];
}
```

<Note>
  Image fields have different jobs:

  * `cdn_image_url` is the optimized storefront cover for cards and lists
  * `detail_image_url` is the higher-resolution primary image for product detail pages
  * `images[]` contains the gallery

  If you run a headless storefront, keep cards on `cdn_image_url` and switch your PDP hero and gallery to `detail_image_url`.
</Note>

### Cart types

```typescript theme={"system"}
interface CartItem {
  product_id: string;
  variant_id: string;
  quantity: number;
  addons?: CartAddon[];
  custom_fields?: Record<string, string>;
  price_variant_id?: string;
}

interface CartAddon {
  id: string;
  quantity?: number;
}

interface CartAddOptions {
  addons?: CartAddon[];
  custom_fields?: Record<string, string>;
  price_variant_id?: string;
}

interface CartUpdateOptions {
  quantity?: number;
  addons?: CartAddon[];
  custom_fields?: Record<string, string>;
  price_variant_id?: string;
}
```

### Checkout types

```typescript theme={"system"}
interface CheckoutOptions {
  autoRedirect?: boolean;
  locale?: string;
}

interface CheckoutResult {
  success: boolean;
  redirectUrl?: string;
  message?: string;
}

interface CouponValidation {
  valid: boolean;
  discount?: number;
  discount_type?: 'percentage' | 'fixed';
  message?: string;
}
```

### Invoice types

```typescript theme={"system"}
interface Invoice {
  uniqid: string;
  status: string;
  total: number;
  currency: string;
  gateway?: string;
  products: InvoiceProduct[];
  created_at: string;
}

interface InvoiceProduct {
  product_id: string;
  title: string;
  quantity: number;
  price: number;
}
```

### Review types

```typescript theme={"system"}
interface Feedback {
  id: string;
  rating: number;
  comment?: string;
  created_at: string;
}
```

### Error types

```typescript theme={"system"}
class ShoppexError extends Error {
  readonly code: string;
  readonly statusCode?: number;
}

class NotInitializedError extends ShoppexError {
  // Thrown when SDK methods are called before init()
  // code: 'NOT_INITIALIZED'
}

class NetworkError extends ShoppexError {
  // Thrown on HTTP/network failures
  // code: 'NETWORK_ERROR'
}

class ValidationError extends ShoppexError {
  // Thrown on input validation failures
  // code: 'VALIDATION_ERROR'
  readonly invalidFields?: string[];
}

class CartError extends ShoppexError {
  // Thrown on cart operation errors
  // code: 'BASKET_ERROR'
}
```

### Usage with TypeScript

```typescript theme={"system"}
import shoppex, {
  type Product,
  type ProductVariant,
  type CartItem,
  type Invoice,
  type SDKResponse
} from '@shoppexio/storefront';

// Initialize
shoppex.init('my-store');

// Typed responses
async function loadProducts(): Promise<Product[]> {
  const response: SDKResponse<Product[]> = await shoppex.getProducts();

  if (!response.success || !response.data) {
    throw new Error(response.message || 'Failed to load products');
  }

  return response.data;
}

// Type-safe cart operations
function addProductToCart(product: Product, variant?: ProductVariant): void {
  shoppex.addToCart(
    product.uniqid,
    variant?.id ?? '',
    1
  );
}

// Typed invoice handling
async function checkOrderStatus(invoiceId: string): Promise<string> {
  const { data } = await shoppex.getInvoiceStatus(invoiceId);
  return data.status;
}
```
