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

# Products

> Reference for the ProductsService class in the Tybrite SDK.

The `ProductsService` class (accessed via `client.products`) is the core discovery engine. It provides high-performance access to your catalog with advanced filtering, **hierarchical variant structures**, and a unified **media gallery system**.

<Note>
  **Validation & Rate Limits:** Product IDs must be valid UUIDs — malformed IDs return `400` immediately. All endpoints are rate-limited (publishable key: 1,000 requests/hr per IP; secret key: 10,000 requests/hr per key).
</Note>

<Note>
  **Product identifier:** every product object exposes both `id` and `product_id`. They are the same value — use whichever fits your codebase.
</Note>

## Response Structure Tiers

To optimize performance across different device types, the service implements three response tiers:

| Tier                     | Endpoint       | Structure                                           | Use Case                                       |
| :----------------------- | :------------- | :-------------------------------------------------- | :--------------------------------------------- |
| **Tier 1: Catalog**      | `listProducts` | **Flat**: Root-level default variant data.          | Homepages, Category Grids, Search.             |
| **Tier 2: PDP (Multi)**  | `getProduct`   | **Hierarchical**: Root metadata + `variants` array. | Detail pages where users select options.       |
| **Tier 3: PDP (Simple)** | `getProduct`   | **Flat**: Full detail without `variants` array.     | Single-variant products (no selection needed). |

***

## Catalog Discovery

### `listProducts`

Retrieve a paginated list of products. This method returns **flat structures** (no `variants` array) to keep payloads small.

* **⚡ Performance**: 50-70% smaller than detail endpoints.
* **🖼️ Thumbnails**: Use `thumbnail_url` for fast rendering in lists without iterating arrays.
* **🛒 Quick Add**: Includes default variant data (`price`, `stock`, `sku`) at root level.
* **🔍 Intelligence**: Use the `has_variants` flag to determine if the UI should show "View Options".
* **⏩ Pagination**: Uses cursor-based navigation for stable, high-performance scrolling.

<Note>
  **Lean by default.** The list omits heavy fields a catalog grid rarely needs — the long-form
  `description`, the SEO block (`seo_title` / `seo_description` / `seo_keywords`), raw `attributes` /
  `shipping_info`, and the full `media` arrays — so grids stay fast. Get those on the detail calls
  (`getProduct` / `getProductBySlug`), request them explicitly with `fields` (e.g.
  `fields: 'name,price,description,seo_title'`), or pass `full: true` to return the complete object
  for every row. `thumbnail_url`, `price`, `stock`, `has_variants` and the taxonomy fields are always
  present.
</Note>

```typescript theme={null}
const { products, pagination } = await client.products.listProducts({
  search: 'Sony',
  limit: 20,
  fields: 'name,price,stock,has_variants,product_slug,thumbnail_url'
});

// For subsequent pages, pass the cursor from the previous response:
// const next = await client.products.listProducts({ limit: 20, cursor: pagination.next_cursor });

console.log(`Has more: ${pagination.has_more}`);
if (pagination.next_cursor) {
  // Use pagination.next_cursor for the subsequent request
}
```

```json Response theme={null}
{
  "products": [
    {
      "name": "Sony WH-1000XM4",
      "price": 349.99,
      "stock": 79,
      "has_variants": true,
      "product_slug": "sony-wh-1000xm4-a089d-7d425c0a",
      "thumbnail_url": "https://cdn.example.com/.../primary-1774785176485.jpg"
    }
  ],
  "pagination": {
    "limit": 20,
    "next_cursor": "MGM0OThhYzItNGI3Ny00YjAzLWE5YjgtZGJkNDY2MWVmYTU0",
    "has_more": true
  }
}
```

#### Personalized ordering

Pass `personalize: true` together with a signed-in customer's session token (the `x-auth-token`
header) and the page is ordered by how closely each product matches that shopper's preferences,
with the default order as the tiebreak. Without a customer session — or for a shopper who has no
preference signal yet — the order is unchanged, so it is always safe to request.

```typescript theme={null}
const personalized = new Tybrite({
  apiKey: 'tybrite_pk_live_...',
  headers: { 'x-auth-token': customerSessionToken },
});

const { products } = await personalized.products.listProducts({
  personalize: true,
  limit: 20,
  fields: 'name,price,thumbnail_url'
});
// → products re-ordered by how closely each matches this shopper; same row shape as above:
//   [ { name: "Sony WH-1000XM4", price: 349.99, thumbnail_url: "https://cdn.example.com/..." }, ... ]
```

**Filter by brand.** Pass `brand` (case-insensitive exact match) to build a brand landing page —
pair it with `listBrands` below to render a "shop by brand" row, then list the chosen brand's products:

```typescript theme={null}
const { products } = await client.products.listProducts({ brand: 'Samsung', limit: 20 });
```

***

### `listBrands`

Return the catalog's distinct brands, each with a product count, sorted by count (most products
first). Use it to build brand navigation — for example a "featured brands" row after the hero — then
pass a chosen brand to `listProducts({ brand })`.

```typescript theme={null}
const { brands, total } = await client.products.listBrands();
```

```json Response theme={null}
{
  "brands": [
    { "brand": "Samsung", "product_count": 12 },
    { "brand": "Apple", "product_count": 9 },
    { "brand": "Sony", "product_count": 4 }
  ],
  "total": 3
}
```

On a marketplace operator key the brands are aggregated across all active merchants; pass `storeId`
to scope to one merchant.

***

### `getProduct`

Retrieve complete details for a single item. This method handles **Multi-Variant** products by returning a hierarchical structure that eliminates data redundancy and supports **variant-aware media**.

* **Product-Level**: Shared metadata at the root (name, brand, media gallery).
* **Aggregate Data**: `total_stock` (sum of all variants) and `price_range` (`min`/`max` using `selling_price`).
* **Variants Array**: Clean array containing variant-specific attributes (color, size, SKU) and **isolated media** for that specific variation.

```typescript theme={null}
const product = await client.products.getProduct({
  id: '7d425c0a-50e7-4e39-af79-6f7945f9dfa8', // a multi-variant product (Sony WH-1000XM4)
  fields: 'name,thumbnail_url,media,price_range,total_stock,variants.sku,variants.media,variants.variant_attributes'
});

if (product.has_variants) {
  console.log(`Variants available: ${product.variant_count}`);
  console.log(`Price Range: ${product.price_range.min} - ${product.price_range.max}`);

  // Access variant-specific media (e.g., if user selects 'Grey' color)
  const greyVariant = product.variants.find(v => v.variant_attributes?.color === 'Grey');
  console.log(`Grey variation images: ${greyVariant?.media.length}`);
}
```

A multi-variant detail response (here shown for the "Nebula Ceramic Mug", a 3-variant
product) — root metadata plus aggregate `price_range` / `total_stock` and a clean `variants[]`
array, each with its own SKU, attributes, and isolated `media`:

```json Response theme={null}
{
  "product_id": "2b6a066a-25df-4238-9e90-50a4400fc21e",
  "name": "Nebula Ceramic Mug",
  "thumbnail_url": "https://cdn.example.com/seed/apimugblue/400",
  "media": [],
  "has_variants": true,
  "variant_count": 3,
  "total_stock": 397,
  "price_range": { "min": 14.99, "max": 15.55 },
  "variants": [
    { "sku": "API-MUG-WHT-2b6a", "variant_attributes": null, "variant_name": "White", "is_default": true,  "media": [] },
    { "sku": "API-MUG-BLU-2b6a", "variant_attributes": null, "variant_name": "Blue",  "is_default": false, "media": [] },
    { "sku": "API-MUG-BLK-2b6a", "variant_attributes": null, "variant_name": "Black", "is_default": false, "media": [] }
  ]
}
```

***

### `getProductBySlug`

Retrieve a product using its SEO-friendly slug. Identical to `getProduct` in response structure but optimized for PDP routing.

```typescript theme={null}
const product = await client.products.getProductBySlug({
  slug: 'sony-wh-1000xm4-a089d-7d425c0a',
  fields: 'seo_title,seo_description,thumbnail_url,media,price_range,variants.*'
});
// → identical structure to getProduct: root metadata + (for multi-variant products)
//   price_range, total_stock, and a variants[] array. See the getProduct response above.
```

### SEO metadata

Every product carries optional SEO metadata for rendering search-engine-friendly product
detail pages:

* `seo_title` — the `<title>` for the product page.
* `seo_description` — the meta description.
* `seo_keywords` — an array of keywords.

```typescript theme={null}
const product = await client.products.getProductBySlug({ slug: 'sony-wh-1000xm4-a089d-7d425c0a' });
// → product.seo_title, product.seo_description, and product.seo_keywords (string[]) are present
//   when set, e.g. seo_title: "Nebula Ceramic Mug | 350ml Dishwasher Safe",
//   seo_keywords: ["ceramic mug", "stoneware mug", "coffee mug", "dishwasher safe mug"]
document.title = product.seo_title ?? product.name;
// <meta name="description" content={product.seo_description ?? product.description} />
```

These fields are also included in a store's public catalog feed, so a catalog mirrored from
one store into another preserves its SEO metadata.

***

## 📸 Media Gallery System

The media system supports a fully-functional, variant-aware gallery.

### Root-Level Media

* `thumbnail_url`: Represents the primary image for the product. Perfect for simple grids.
* `media`: An array of objects containing all base-level images and videos.

### Variant Media

Each variant in the `variants[]` array can have its own `media` array. This allows you to show specific images when a user selects a particular variation (e.g., rotating the gallery to show the "Midnight Blue" model).

**Media Object Structure:**

| Field        | Type      | Description                                                |
| :----------- | :-------- | :--------------------------------------------------------- |
| `id`         | `uuid`    | Unique identifier for the media item.                      |
| `url`        | `string`  | Full CDN URL for the asset.                                |
| `type`       | `string`  | `image` or `video`.                                        |
| `position`   | `number`  | Explicit sort order (starting from 0).                     |
| `alt_text`   | `string`  | Accessibility text.                                        |
| `is_primary` | `boolean` | Whether this is the main image (used for `thumbnail_url`). |

***

## 🎨 Advanced Field Filtering

Tybrite supports powerful field selection to minimize bandwidth.

### Root-Level Filtering

Exclude large metadata objects by selecting only top-level fields.

```bash theme={null}
fields=product_id,name,brand,thumbnail_url,total_stock,price_range,variants
```

### Nested Array Filtering

Use dot notation to select specific fields **within** nested arrays.

```bash theme={null}
# Get only the SKU and specific media for each variant
fields=name,variants.sku,variants.media,variants.variant_attributes
```

**Supported Fields for Filtering:**

* **Core:** `product_id`, `name`, `sku`, `description`, `thumbnail_url`, `media`
* **Pricing:** `price`, `selling_price`, `sale_price`, `price_range`
* **Inventory:** `stock`, `total_stock`, `last_restocked` (`last_restocked` is not returned by default — request it explicitly)
* **Variants:** `variants.sku`, `variants.selling_price`, `variants.media`, `variants.variant_attributes`

***

## Product Specifications

### `getProductSpecifications`

Retrieve the latest published specifications for a single **variant**. Specifications are stored per
variant, so pass a **variant id** (e.g. a `variant_id` from `getProduct().variants[]`) — **not** a
product id. A product id returns `404`.

```typescript theme={null}
const { specification_data } = await client.products.getProductSpecifications({
  id: '9a47e047-b1b6-4c35-9617-820629e22e04' // a VARIANT UUID (not a product id)
});
```

```json Response theme={null}
{
  "id": "3b16b560-418f-4fe5-a35e-7ce9c214d795",
  "variant_id": "9a47e047-b1b6-4c35-9617-820629e22e04",
  "template_id": "599b4ff3-bda4-463d-a22e-e631d834875a",
  "specification_data": {
    "brand": "Sony",
    "color": "Black",
    "model": "WH-1000XM4",
    "product_type": "Accessory",
    "storage_type": "HDD"
  },
  "status": "published",
  "version": 1,
  "created_at": "2026-06-20T10:05:26.106528+00:00",
  "updated_at": "2026-06-20T10:05:26.106528+00:00"
}
```

<Note>
  Returns `404` ("No specifications found for this product") if the variant has no published
  specification — or if you passed a product id instead of a variant id. To browse all specs across
  the catalog (each tagged with its `variant_id`), use `listProductSpecifications`.
</Note>

***

## Collections

Curated groups of products a merchant highlights on their storefront — a "Featured", "New Arrivals", or "Summer Edit" row. Each collection can carry a banner image, which makes it a natural building block for an image-led homepage.

```typescript theme={null}
// List all active collections
const { collections } = await client.products.listProductCollections({ 
  showOnHomepage: 'true',
  limit: 20
});
// → collections: [
//     { id: "45e28d12-9381-45f6-98bb-cb3a34c0a215", name: "Featured", slug: "featured",
//       description: "Hand-picked products to highlight on your storefront.",
//       collection_type: "featured_collections", is_active: true,
//       show_on_homepage: true, display_priority: 0,
//       image: "https://media.example.com/.../featured-desktop.jpg",
//       image_mobile: "https://media.example.com/.../featured-mobile.jpg" },
//     { id: "e9810818-5051-4b74-a53b-2ff843b4f435", name: "New Arrivals", slug: "new-arrivals",
//       collection_type: "new_arrivals", is_active: true, image: null, image_mobile: null, ... }
//   ]
```

`image` is the collection's banner and the image to use on every viewport; `image_mobile` is an optional phone-sized crop. Render the mobile image on small screens when it is set and fall back to `image` otherwise — a `<picture>` element with a mobile `<source>` handles this cleanly. Either field is `null` when the merchant has not added artwork.

```typescript theme={null}

// Get items in a specific collection (returns all items, ordered by sort_order)
// Each item has: product_id, sort_order, and an embedded product object
const { items } = await client.products.getProductCollectionItems({
  id: '45e28d12-9381-45f6-98bb-cb3a34c0a215' // e.g. the "Featured" collection
});
// → items: [
//     { product_id: "7d425c0a-50e7-4e39-af79-6f7945f9dfa8", sort_order: 0,
//       product: { id: "7d425c0a-50e7-4e39-af79-6f7945f9dfa8", name: "Sony WH-1000XM4",
//                  slug: "sony-wh-1000xm4-a089d-7d425c0a", brand: "Sony",
//                  has_variants: true, is_active: true } },
//     ...
//   ]
// (an empty collection returns { items: [] })

for (const item of items) {
  console.log(`${item.product.name} — sort order: ${item.sort_order}`);
}
```

***

## Response Codes

All product endpoints are `GET` and accept both **publishable** and **secret** keys.

| Code  | Meaning                                                                                                                            |
| :---- | :--------------------------------------------------------------------------------------------------------------------------------- |
| `200` | Success.                                                                                                                           |
| `400` | Invalid request — malformed UUID, invalid slug, or unrecognized field in the `fields` parameter.                                   |
| `401` | Invalid or missing API key.                                                                                                        |
| `404` | Product, collection, or specification not found (single-resource endpoints only). List endpoints return `200` with an empty array. |
| `429` | Rate limit exceeded (1,000/hr per IP for `pk_`; 10,000/hr per key for `sk_`).                                                      |
| `500` | Internal server error.                                                                                                             |
