> ## 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.

# Products API

> Fetch products, variants, and categories

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>

  <Note>
    Product prices are returned as `string` types to preserve decimal precision. Always use `shoppex.formatPrice()` for display — do not perform arithmetic directly on price strings.
  </Note>

  <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 (e.g. 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 (e.g., size, color)
  </ResponseField>

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

  <ResponseField name="price_variants" type="PriceVariant[]">
    Price-based variants (e.g., 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>

<Info>
  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`, you should switch that hero to `detail_image_url` to get the higher-resolution image.
</Info>

### 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 (e.g. "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 will see `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`, and `/v1/storefront/products/shop/:name` 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 like size or color that don't 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
});
```

## 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
}
```

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Cart API" icon="cart-shopping" href="/sdk/api/cart">
    Add products to the shopping cart
  </Card>

  <Card title="Store API" icon="store" href="/sdk/api/store">
    Fetch store metadata and branding
  </Card>
</CardGroup>
